原创

并查集(Java版)




并查集 Union Find

对于一组数据,主要支持两个动作:

·union(p,q)
·find(p)

用来回答一个问题

·isConnected(p,q)

一、Quick Find

并查集的基本数据表示

0 1 2 3 4 5 6 7 8 9


0 0 0 0 0 1 1 1 1 1

Find

id 0 1 2 3 4 5 6 7 8 9


0 1 0 1 0 1 0 1 0 1

Quick Find 下的 Find 时间复杂度O(1)

Quick Find 下的 Union 时间复杂度O(n)

Quick Find代码实现:

public class UnionFind {

    private int[] id;
    private int count;

    public UnionFind(int n){
        count = n;
        id = new int[n];
        for(int i = 0; i < n; i++){
            id[i] = i;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        return id[p];
    }

    public boolean isConnected(int p, int q) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements( int p , int q ) throws Exception {
        int pID = find(p);
        int qID = find(q);

        if( pID == qID ){
            return;
        }
        for( int i = 0; i < count ; i ++ ){
            if ( id[i] == pID )
                id[i] = qID;
        }
    }

	//测试
    public static void testUF1(int n) throws Exception {
        Random random = new Random();
        UnionFind uf = new UnionFind(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF1, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 100000;
        testUF1(n);
    }

}

测试结果:

n = 10000

UF1, 20000ops,0.058

n = 100000

UF1, 200000ops,5.083

二、Quick Union

并查集的另外一种实现思路 -- 将每一个元素,看作是一个节点

Quick Union下的数据表示

0 1 2 3 4 5 6 7 8 9


parent 0 1 2 3 4 5 6 7 8 9

Quick Union代码实现:

public class UnionFind2 {

    private int[] parent;
    private int count;

    public UnionFind2(int n){
        count = n;
        parent = new int[n];
        for( int i = 0; i < n; i++ ){
            parent[i] = i;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        while( p != parent[p])
            p = parent[p];
        return p;
    }

    public boolean isConnected(int p , int q ) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements(int p , int q ) throws Exception {

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        parent[pRoot] = qRoot;
    }

    public static void testUF2(int n) throws Exception {
        Random random = new Random();
        UnionFind2 uf = new UnionFind2(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF2, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 10000;
        UnionFind.testUF1(n);
        testUF2(n);
    }

}

测试结果:

n = 10000

UF1, 20000ops,0.062
UF2, 20000ops,0.031

n = 100000

UF1, 200000ops,4.726
UF2, 200000ops,10.346

2.1 基于Size的优化:

给每一个根节点维护一个size大小,表示这颗树的大小。只需要在合并的时候把size小的合并到size大的树上,即把size小的根节点的父节点赋值为size大的根节点,其它不需要改变。

代码实现:

public class UnionFind3 {

    private int[] parent;
    private int[] size; //size[i]表示以i为根的集合中元素个数
    private int count;

    public UnionFind3(int n){
        count = n;
        parent = new int[n];
        size = new int[n];
        for( int i = 0; i < n; i++ ){
            parent[i] = i;
            size[i] = 1;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        while( p != parent[p])
            p = parent[p];
        return p;
    }

    public boolean isConnected(int p , int q ) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements(int p , int q ) throws Exception {

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        if (size[pRoot] < size[qRoot]) {
            parent[pRoot] = qRoot;
            size[qRoot] += size[pRoot];
        }else{
            parent[qRoot] = pRoot;
            size[pRoot] += size[qRoot];
        }
    }

    public static void testUF3(int n) throws Exception {
        Random random = new Random();
        UnionFind3 uf = new UnionFind3(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF3, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 10000;
        UnionFind.testUF1(n);
        UnionFind2.testUF2(n);
        testUF3(n);
    }

}

测试结果:

n = 10000

UF1, 20000ops,0.059
UF2, 20000ops,0.036
UF3, 20000ops,0.003

n = 100000

UF1, 200000ops,4.666
UF2, 200000ops,10.193
UF3, 200000ops,0.019

2.2 基于Rank的优化:

基于Rank的优化,rank[i]表示根节点为i的树的高度。

把rank值小的根节点的父节点赋值为rank值大的根节点,如果两个rank值相同,则选择任意一个节点的根节点赋值为另一个根节点的值,并把另一个节点的rank值加1。

public class UnionFind4 {

    private int[] parent;
    private int[] rank; //rank[i]表示以i为根的集合所表示的树的层数
    private int count;

    public UnionFind4(int n){
        count = n;
        parent = new int[n];
        rank = new int[n];
        for( int i = 0; i < n; i++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        while( p != parent[p])
            p = parent[p];
        return p;
    }

    public boolean isConnected(int p , int q ) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements(int p , int q ) throws Exception {

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        if (rank[pRoot] < rank[qRoot]) {
            parent[pRoot] = qRoot;
        }else if (rank[qRoot] < rank[pRoot]){
            parent[qRoot] = pRoot;
        }else { //rank[pRoot] == rank[qRoot]
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }

    public static void testUF4(int n) throws Exception {
        Random random = new Random();
        UnionFind4 uf = new UnionFind4(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF4, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 10000;
        UnionFind.testUF1(n);
        UnionFind2.testUF2(n);
        UnionFind3.testUF3(n);
        testUF4(n);
    }

}

测试结果:

n = 10000

UF1, 20000ops,0.066
UF2, 20000ops,0.033
UF3, 20000ops,0.005
UF4, 20000ops,0.005

n = 100000

UF1, 200000ops,4.865
UF2, 200000ops,10.325
UF3, 200000ops,0.015
UF4, 200000ops,0.014

2.3 路径压缩

前面两种优化方法,优化的地方都集中在union这个操作上。但实际上,在find的这个操作过程中,其实我们也是可以优化的。 在find的过程中,从底向上,如果没有找到根的话,就想办法把这些节点再向上挪一挪,那么这个过程就叫做路径压缩。

2.3.1 方法1

代码实现:

private int[] parent;
    private int[] rank; //rank[i]表示以i为根的集合所表示的树的层数
    private int count;

    public UnionFind5(int n){
        count = n;
        parent = new int[n];
        rank = new int[n];
        for( int i = 0; i < n; i++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        while( p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;
    }

    public boolean isConnected(int p , int q ) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements(int p , int q ) throws Exception {

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        if (rank[pRoot] < rank[qRoot]) {
            parent[pRoot] = qRoot;
        }else if (rank[qRoot] < rank[pRoot]){
            parent[qRoot] = pRoot;
        }else { //rank[pRoot] == rank[qRoot]
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }

    public static void testUF5(int n) throws Exception {
        Random random = new Random();
        UnionFind5 uf = new UnionFind5(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF5, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 10000;
        UnionFind.testUF1(n);
        UnionFind2.testUF2(n);
        UnionFind3.testUF3(n);
        UnionFind4.testUF4(n);
        testUF5(n);
    }

测试结果:

n = 10000

UF1, 20000ops,0.079
UF2, 20000ops,0.043
UF3, 20000ops,0.003
UF4, 20000ops,0.004
UF5, 20000ops,0.003

n = 100000

UF1, 200000ops,4.73
UF2, 200000ops,10.115
UF3, 200000ops,0.016
UF4, 200000ops,0.015
UF5, 200000ops,0.013

2.3.2 方法2

代码实现:

public class UnionFind5 {

    private int[] parent;
    private int[] rank; //rank[i]表示以i为根的集合所表示的树的层数
    private int count;

    public UnionFind5(int n){
        count = n;
        parent = new int[n];
        rank = new int[n];
        for( int i = 0; i < n; i++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    public int find(int p) throws Exception {
        if(p < 0 || p >= count)
            throw new Exception("p非法");
        /*while( p != parent[p]) {
            parent[p] = parent[parent[p]];
            p = parent[p];
        }
        return p;*/
        if( p != parent[p]){
            parent[p] = find(parent[p]);
        }
        return parent[p];
    }

    public boolean isConnected(int p , int q ) throws Exception {
        return find(p) == find(q);
    }

    public void unionElements(int p , int q ) throws Exception {

        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        if (rank[pRoot] < rank[qRoot]) {
            parent[pRoot] = qRoot;
        }else if (rank[qRoot] < rank[pRoot]){
            parent[qRoot] = pRoot;
        }else { //rank[pRoot] == rank[qRoot]
            parent[qRoot] = pRoot;
            rank[pRoot]++;
        }
    }

    public static void testUF5(int n) throws Exception {
        Random random = new Random();
        UnionFind5 uf = new UnionFind5(n);
        long startTime = System.currentTimeMillis();
        for( int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.unionElements(a,b);
        }
        for(int i = 0 ; i < n ; i ++ ){
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            uf.isConnected(a,b);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("UF5, " + 2 * n + "ops," + (endTime - startTime) / 1000.0);

    }

    public static void main(String[] args) throws Exception {
        int n = 100000;
        UnionFind.testUF1(n);
        UnionFind2.testUF2(n);
        UnionFind3.testUF3(n);
        UnionFind4.testUF4(n);
        testUF5(n);
    }

}

测试结果:

n = 10000

UF1, 20000ops,0.067
UF2, 20000ops,0.04
UF3, 20000ops,0.004
UF4, 20000ops,0.004
UF5, 20000ops,0.003

n = 100000

UF1, 200000ops,4.723
UF2, 200000ops,10.439
UF3, 200000ops,0.014
UF4, 200000ops,0.014
UF5, 200000ops,0.013
数据结构
Java
  • 作者:CodeC.C(联系作者)
  • 发表时间:2020-09-19 15:10:42
  • 评论  null  条