1. 程式人生 > 實用技巧 >12-並查集 UnionFind

12-並查集 UnionFind

目錄


1、簡介

​ 並查集是一種樹型的資料結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題

  • union(p,q),合併兩個元素所在的集合
  • isConnected(p,q),查詢兩個元素是否屬於一個集合

2、實現

2.1、實現思路

  • 將每一個元素看作是一個結點
  • 獨立的結點指向自己本身
  • 合併兩個結點所屬集合,只需要讓兩個結點的根結點連線即可
  • 判斷兩個結點是否屬於一個集合,只需要檢視它們的根結點是否為同一個結點即可

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、初始狀態

每一個結點指向自己,這種狀態可以使用陣列進行表示:每一個結點對應陣列中的相應索引處的值。

2.4、union操作

  • union 4,3
  • union 3,8
  • union 9,4
  • union 2,1
  • union 5,0
  • union 7,2
  • union 6,2

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

顯然,這樣的合併操作有可能會不斷增加樹的高度,進而導致isConnected操作的複雜度過大。

解決方法:讓結點數少的樹的根結點指向結點數多的樹的根結點

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

很明顯,按照之前的優化方式,樹的高度將增大。

解決方法:讓高度小的樹的根結點指向高度大的的樹的根結點
與前面基於size的優化相比,基於rank的優化可以減少樹的高度增長的概率。

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、路徑壓縮

理想情況下,一棵樹的形狀應該是③ :樹的高度儘可能的小。

為此我們需要進行路徑壓縮,減小樹的高度,此過程設計在find方法中,因為在合併兩個結點前要進行find操作。

4.1、壓縮方式一

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

4.2、壓縮方式二

// 遞迴實現路徑壓縮
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);
}