12-并查集 UnionFind

学习资源:慕课网liyubobobo老师的《玩儿转数据结构》


1、简介

​ 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题

  • union(p,q),合并两个元素所在的集合
  • isConnected(p,q),查询两个元素是否属于一个集合

2、实现

2.1、实现思路

  • 将每一个元素看作是一个结点
  • 独立的结点指向自己本身
  • 合并两个结点所属集合,只需要让两个结点的根结点连接即可
image-20200624111947418
  • 判断两个结点是否属于一个集合,只需要查看它们的根结点是否为同一个结点即可

2.2、接口

public interface unionFind {

    //p,q不是具体的元素,而是像元素在数组的索引
    
    int getSize();
    // 查看元素p和元素q是否属于一个集合
    boolean isConnected(int p, int q);
    // 合并元素p和元素q所属的集合
    void unionElement(int p, int q);	
}

2.3、初始状态

每一个结点指向自己,这种状态可以使用数组进行表示:每一个结点对应数组中的相应索引处的值。

image-20200624113309384

2.4、union操作

  • union 4,3
image-20200624113827626
  • union 3,8
image-20200624113805626
  • union 9,4
image-20200624113747493
  • union 2,1
image-20200624113936933
  • union 5,0
image-20200624114031317
  • union 7,2
image-20200624114125689
  • union 6,2
image-20200624114217074

2.5、代码

public class UnionFind_2 implements unionFind {

    private int[] parent;

    // 每一个元素单独属于一个集合
    public UnionFind_2(int size) {

        parent = new int[size];
        for(int i = 0; i < size; i++){
            parent[i] = i;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }

    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @Override
    public void unionElement(int p, int q) {

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

        if(pRoot == qRoot){
            return;
        }

        parent[pRoot] = qRoot;
    }

    // 查找元素p对应的集合编号
    private int find(int p){

        while (p != parent[p])
            p = parent[p];
        return p;
    }
}

3、代码优化

3.1、基于size的优化

union 8,9

image-20200630095620908

显然,这样的合并操作有可能会不断增加树的高度,进而导致isConnected操作的复杂度过大。

解决方法:让结点数少的树的根结点指向结点数多的树的根结点

image-20200630095700373
public class UnionFind_3 implements unionFind {

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

    public UnionFind_3(int size) {

        parent = new int[size];
        this.size = new int[size];
        for(int i = 0; i < size; i++){
            
            parent[i] = i;
            this.size[i] = 1;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }

    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @Override
    public void unionElement(int p, int q) {

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

        if(pRoot == qRoot){
            return;
        }

        if(size[pRoot] < size[qRoot]){

            parent[pRoot] = qRoot;
            size[qRoot] += size[qRoot];
        }
        else{
            parent[qRoot] = pRoot;
            size[pRoot] += size[qRoot];
        }
    }

    private int find(int p){

        while (p != parent[p])
            p = parent[p];
        return p;
    }
}

3.2、基于rank的优化

union 7,8

image-20200703153053193

很明显,按照之前的优化方式,树的高度将增大。

解决方法:让高度小的树的根结点指向高度大的的树的根结点
与前面基于size的优化相比,基于rank的优化可以减少树的高度增长的概率。

image-20200703153621674
public class UnionFind_4 implements unionFind {

    private int[] parent;
    private int[] rank;     // rank[i] 表示以i为根的集合所表示的树的高度

    public UnionFind_4(int size) {

        parent = new int[size];
        this.rank = new int[size];
        for(int i = 0; i < size; i++){
            parent[i] = i;
            rank[i] = 1;
        }
    }

    @Override
    public int getSize() {
        return parent.length;
    }

    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    @Override
    public void unionElement(int p, int q) {

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

        if(pRoot == qRoot){
            return;
        }

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

    private int find(int p){

        while (p != parent[p])
            p = parent[p];
        return p;
    }
}

4、路径压缩

image-20200703154938873

理想情况下,一棵树的形状应该是③ :树的高度尽可能的小。

为此我们需要进行路径压缩,减小树的高度,此过程设计在find方法中,因为在合并两个结点前要进行find操作。

4.1、压缩方式一

image-20200703155843933
private int find(int p){
    
    while (p != parent[p]){
        
        parent[p] = parent[parent[p]];
        p = parent[p];
    }
    return p;
}

4.2、压缩方式二

image-20200703160331995
// 递归实现路径压缩
private int find(int p){
    
    if(p != parent[p]){
        parent[p] = find(parent[p]);
    }
    return parent[p];
}

5、测试

private static long getRunTimeNano(UnionFind unionFind, int times){
    int size = unionFind.getSize();
    Random random = new Random();
    long startTime = System.nanoTime();
    for(int i=0; i<times; i++){
        int a = random.nextInt(size);
        int b = random.nextInt(size);
        unionFind.unionElement(a, b);
    }
    for(int i=0; i<times; i++){
        int a = random.nextInt(size);
        int b = random.nextInt(size);
        unionFind.isConnected(a, b);
    }
    long endTime = System.nanoTime();
    return endTime - startTime;
}

public static void main(String[] args) {
    int size = 100000;
    int times = 100000;
    UnionFind_6 unionFind_6 = new UnionFind_6(size);
    long runTime = getRunTimeMills(unionFind_6, times);
    System.out.println(runTime);
}
posted @ 2020-07-03 19:13  卡文迪雨  阅读(142)  评论(0编辑  收藏  举报