1. 程式人生 > >玩轉資料結構(18)-- 並查集

玩轉資料結構(18)-- 並查集

並查集(Union Find)

一、概述

由孩子節點指向父親節點的樹結構,解決連線問題,如圖來判斷兩個點之間是否是連線的

並查集:可以快速判斷網路中節點間的連線狀態【網路:抽象概念,使用者之間形成的網路】可以高效回答連線問題的資料結構

對於一組資料,主要支援兩個動作:

1.uoion(p,q)   --並,傳入資料 p 和 q,在並查集內部將這兩個資料,以及他們所在的集合合併起來

2.isConnected(p,q)   ---查詢 ,對於給定的兩個資料 p 和 q 是否屬於同一個集合

二、並查集的基本資料表示

第一版Union-Find本質就是一個數組

 對 10 個元素 (0-9)分成 2 個集合,其中 (0-4) 這 5 個元素為 集合0 ;(5-9) 這 5 個元素為 集合1

 

或者

在 isConnected(p,q)   ---查詢 中,只需要檢視  p 和 q 所對應的 Id 值是否相等即可,將檢視  p 和 q 背後的 Id 是誰的過程抽象為函式:find(p) == find(q) ,這種方式為 Quick Find,其時間複雜度為 O(1)

但 Quick Find 中的 uoion(p,q)   --並  時間複雜度為 O(n);合併過程需要遍歷一遍所有元素, 將兩個元素的所屬集合編號合併

程式碼實現:

UF.java

public interface UF {	//定義介面

    int getSize();	//元素數量
    boolean isConnected(int p, int q);
    void unionElements(int p, int q);
}

UnionFind1.java

// 我們的第一版Union-Find
public class UnionFind1 implements UF {

    private int[] id;    // 我們的第一版Union-Find本質就是一個數組

    public UnionFind1(int size) {

        id = new int[size];

        // 初始化, 每一個id[i]指向自己, 沒有合併的元素
        for (int i = 0; i < size; i++)
            id[i] = i;
    }

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

    // 查詢元素p所對應的集合編號
    // O(1)複雜度
    private int find(int p) {
        if(p < 0 || p >= id.length)
            throw new IllegalArgumentException("p is out of bound.");

        return id[p];
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(1)複雜度
    @Override
    public boolean isConnected(int p, int q) {
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(n) 複雜度
    @Override
    public void unionElements(int p, int q) {

        int pID = find(p);
        int qID = find(q);

        if (pID == qID)
            return;

        // 合併過程需要遍歷一遍所有元素, 將兩個元素的所屬集合編號合併
        for (int i = 0; i < id.length; i++)
            if (id[i] == pID)
                id[i] = qID;
    }
}

第二版Union-Find, 使用一個數組構建一棵指向父節點的樹

將每一個元素,看做是一個節點;其中,節點 3 指向其父節點 2,2 為根節點,其指標指向自己即可;

節點1 和 節點3 合併,則讓 節點1 所對應的指標指向 節點3 所在樹的根節點,即 節點2;

如果讓 節點7 和 節點2 合併 ,即讓 節點7 所在的 根節點5 ,指向 節點2 即可;

如果讓 節點7 和 節點3 合併 ,即讓 節點7 所在的 根節點5 ,指向 節點3 所在樹的 根節點2 即可;

 Quick Union:並查集不是一個樹結構,而是 森林結構 ,裡面存在很多樹 ,在初始親情況下有 10 棵樹,每棵樹只有一個節點;

如果執行 union 4,3 操作

再執行 union 3,8

再執行 union 6,5

再執行 union 9,4[讓節點 9 指向 節點4 所在的根節點]

在Quick Union 中,查操作 與 並操作 的時間複雜度均為 O(h) ,h為樹的高度

程式碼實現:

UF.java

public interface UF {	//定義介面

    int getSize();	//元素數量
    boolean isConnected(int p, int q);
    void unionElements(int p, int q);
}

UnionFind2.java

// 我們的第二版Union-Find
public class UnionFind2 implements UF {

    // 我們的第二版Union-Find, 使用一個數組構建一棵指向父節點的樹
    // parent[i]表示第一個元素所指向的父節點
    private int[] parent;

    // 建構函式
    public UnionFind2(int size){

        parent = new int[size];

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for( int i = 0 ; i < size ; i ++ )
            parent[i] = i;
    }

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

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不斷去查詢自己的父親節點, 直到到達根節點
        // 根節點的特點: parent[p] == p
        while(p != parent[p])
            p = parent[p];
        return p;
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

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

        if( pRoot == qRoot )
            return;

        parent[pRoot] = qRoot;
    }
}

三、優化

基於size的優化

並查集的實現由於對真正合並的元素不做形狀上的判斷,這個合併的過程會不斷增加樹的高度,甚至演化為 連結串列;

解決辦法:考慮(size)當前這棵樹整體有多少個節點,例如 union 4,9,正常操作如下圖

這時的高度為 4,但完全可以讓 9 指向 4 所在的根節點 8 ,即 9 指向 8;高度變為 3

程式碼實現:UnionFind3.java

// 我們的第三版Union-Find
public class UnionFind3 implements UF{

    private int[] parent; // parent[i]表示第一個元素所指向的父節點
    private int[] sz;     // sz[i]表示以i為根的集合中元素個數【新增程式碼】

    // 建構函式
    public UnionFind3(int size){

        parent = new int[size];
        sz = new int[size];  //【新增程式碼】

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for(int i = 0 ; i < size ; i ++){
            parent[i] = i;
            sz[i] = 1;  //【新增程式碼】
        }
    }

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

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不斷去查詢自己的父親節點, 直到到達根節點
        // 根節點的特點: parent[p] == p
        while( p != parent[p] )
            p = parent[p];
        return p;
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

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

        if(pRoot == qRoot)	
            return;

        // 根據兩個元素所在樹的元素個數不同判斷合併方向
        // 將元素個數少的集合合併到元素個數多的集合上
        if(sz[pRoot] < sz[qRoot]){		//【新增程式碼】
            parent[pRoot] = qRoot;
            sz[qRoot] += sz[pRoot];
        }
        else{ // sz[qRoot] <= sz[pRoot]
            parent[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

檢測時間複雜度:Main.java

import java.util.Random;

public class Main {

    private static double testUF(UF uf, int m){

        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();


        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a, b);
        }

        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a, b);
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }

    public static void main(String[] args) {

        // UnionFind1 慢於 UnionFind2
//        int size = 100000;
//        int m = 10000;

        // UnionFind2 慢於 UnionFind1, 但UnionFind3最快
        int size = 100000;
        int m = 100000;

        UnionFind1 uf1 = new UnionFind1(size);
        System.out.println("UnionFind1 : " + testUF(uf1, m) + " s");

        UnionFind2 uf2 = new UnionFind2(size);
        System.out.println("UnionFind2 : " + testUF(uf2, m) + " s");

        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("UnionFind3 : " + testUF(uf3, m) + " s");
    }
}

輸出:如此優化,效能得到極大提升

 基於rank的優化【樹的高度】

執行 union 4,2 ,以 size 優化方式執行,高度變高了

更合理的方式: 在每一個節點上記錄以這個節點為根的對應的樹,其最大深度為多少,在真正合並的時候,應該使用深度比較低的那棵樹向深度比較高的樹進行合併;整體更加合理;稱為 基於rank 的優化,k[i] 表示根節點為 i 的樹的高度

程式碼實現:

UnionFind4.java

// 我們的第四版Union-Find
public class UnionFind4 implements UF {

    private int[] rank;   // rank[i]表示以i為根的集合所表示的樹的層數
    private int[] parent; // parent[i]表示第i個元素所指向的父節點

    // 建構函式
    public UnionFind4(int size){

        rank = new int[size];
        parent = new int[size];

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // 不斷去查詢自己的父親節點, 直到到達根節點
        // 根節點的特點: parent[p] == p
        while(p != parent[p])
            p = parent[p];
        return p;
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

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

        if( pRoot == qRoot )
            return;

        // 根據兩個元素所在樹的rank不同判斷合併方向
        // 將rank低的集合合併到rank高的集合上
        if(rank[pRoot] < rank[qRoot])
            parent[pRoot] = qRoot;
        else if(rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
            parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此時, 我維護rank的值
        }
    }
}

測試時間複雜度:Main.java

import java.util.Random;

public class Main {

    private static double testUF(UF uf, int m){

        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();


        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a, b);
        }

        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a, b);
        }

        long endTime = System.nanoTime();

        return (endTime - startTime) / 1000000000.0;
    }

    public static void main(String[] args) {

        int size = 10000000;
        int m = 10000000;

//        UnionFind1 uf1 = new UnionFind1(size);
//        System.out.println("UnionFind1 : " + testUF(uf1, m) + " s");
//
//        UnionFind2 uf2 = new UnionFind2(size);
//        System.out.println("UnionFind2 : " + testUF(uf2, m) + " s");

        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("UnionFind3 : " + testUF(uf3, m) + " s");

        UnionFind4 uf4 = new UnionFind4(size);
        System.out.println("UnionFind4 : " + testUF(uf4, m) + " s");
    }
}

輸出:

路徑壓縮(Path Compression)

由上述幾種優化方式,知

查詢節點:左側的高度最大,其執行速度最慢;下側的高度最小,其執行速度最快;故將 高度大的樹 壓縮成為 高度小的樹 稱為路徑壓縮

對並查集來說,每一個節點其子樹是沒有限制的,故理想情況下,希望樹的形態如下側那樣(根節點在第一層,其餘節點均在第二層),但很難實現嗎,通常只要追求高度減小即可提高執行速率;

壓縮過程:find 4  【在查詢過程中,壓縮路徑,使高度變小】

1.

2.

3.

程式碼實現:

UnionFind5.java

// 我們的第五版Union-Find
public class UnionFind5 implements UF {

    // rank[i]表示以i為根的集合所表示的樹的層數
    // 在後續的程式碼中, 我們並不會維護rank的語意, 也就是rank的值在路徑壓縮的過程中, 有可能不在是樹的層數值
    // 這也是我們的rank不叫height或者depth的原因, 他只是作為比較的一個標準
    private int[] rank;
    private int[] parent; // parent[i]表示第i個元素所指向的父節點

    // 建構函式
    public UnionFind5(int size){

        rank = new int[size];
        parent = new int[size];

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

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

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

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

        if( pRoot == qRoot )
            return;

        // 根據兩個元素所在樹的rank不同判斷合併方向
        // 將rank低的集合合併到rank高的集合上
        if( rank[pRoot] < rank[qRoot] )
            parent[pRoot] = qRoot;
        else if( rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
            parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此時, 我維護rank的值
        }
    }
}

測試:Main.java

import java.util.Random;

public class Main {

    private static double testUF(UF uf, int m){

        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();


        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a, b);
        }

        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a, b);
        }

        long endTime = System.nanoTime();

        double time = (endTime - startTime) / 1000000000.0;
        return time;
    }

    public static void main(String[] args) {

        int size = 10000000;
        int m = 10000000;

//        UnionFind1 uf1 = new UnionFind1(size);
//        System.out.println("UnionFind1 : " + testUF(uf1, m) + " s");
//
//        UnionFind2 uf2 = new UnionFind2(size);
//        System.out.println("UnionFind2 : " + testUF(uf2, m) + " s");

        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("UnionFind3 : " + testUF(uf3, m) + " s");

        UnionFind4 uf4 = new UnionFind4(size);
        System.out.println("UnionFind4 : " + testUF(uf4, m) + " s");

        UnionFind5 uf5 = new UnionFind5(size);
        System.out.println("UnionFind5 : " + testUF(uf5, m) + " s");
    }
}

輸出:、

理想狀況:通過 遞迴 實現

程式碼實現:UnionFind6.java

// 我們的第六版Union-Find
public class UnionFind6 implements UF {

    // rank[i]表示以i為根的集合所表示的樹的層數
    // 在後續的程式碼中, 我們並不會維護rank的語意, 也就是rank的值在路徑壓縮的過程中, 有可能不在是樹的層數值
    // 這也是我們的rank不叫height或者depth的原因, 他只是作為比較的一個標準
    private int[] rank;
    private int[] parent; // parent[i]表示第i個元素所指向的父節點

    // 建構函式
    public UnionFind6(int size){

        rank = new int[size];
        parent = new int[size];

        // 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
        for( int i = 0 ; i < size ; i ++ ){
            parent[i] = i;
            rank[i] = 1;
        }
    }

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

    // 查詢過程, 查詢元素p所對應的集合編號
    // O(h)複雜度, h為樹的高度
    private int find(int p){
        if(p < 0 || p >= parent.length)
            throw new IllegalArgumentException("p is out of bound.");

        // path compression 2, 遞迴演算法
        if(p != parent[p])
            parent[p] = find(parent[p]);
        return parent[p];
    }

    // 檢視元素p和元素q是否所屬一個集合
    // O(h)複雜度, h為樹的高度
    @Override
    public boolean isConnected( int p , int q ){
        return find(p) == find(q);
    }

    // 合併元素p和元素q所屬的集合
    // O(h)複雜度, h為樹的高度
    @Override
    public void unionElements(int p, int q){

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

        if( pRoot == qRoot )
            return;

        // 根據兩個元素所在樹的rank不同判斷合併方向
        // 將rank低的集合合併到rank高的集合上
        if( rank[pRoot] < rank[qRoot] )
            parent[pRoot] = qRoot;
        else if( rank[qRoot] < rank[pRoot])
            parent[qRoot] = pRoot;
        else{ // rank[pRoot] == rank[qRoot]
            parent[pRoot] = qRoot;
            rank[qRoot] += 1;   // 此時, 我維護rank的值
        }
    }
}

測試:Main.java

import java.util.Random;

public class Main {

    private static double testUF(UF uf, int m){

        int size = uf.getSize();
        Random random = new Random();

        long startTime = System.nanoTime();


        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.unionElements(a, b);
        }

        for(int i = 0 ; i < m ; i ++){
            int a = random.nextInt(size);
            int b = random.nextInt(size);
            uf.isConnected(a, b);
        }

        long endTime = System.nanoTime();

        double time = (endTime - startTime) / 1000000000.0;
        return time;
    }

    public static void main(String[] args) {

        int size = 10000000;
        int m = 10000000;

//        UnionFind1 uf1 = new UnionFind1(size);
//        System.out.println("UnionFind1 : " + testUF(uf1, m) + " s");
//
//        UnionFind2 uf2 = new UnionFind2(size);
//        System.out.println("UnionFind2 : " + testUF(uf2, m) + " s");

        UnionFind3 uf3 = new UnionFind3(size);
        System.out.println("UnionFind3 : " + testUF(uf3, m) + " s");

        UnionFind4 uf4 = new UnionFind4(size);
        System.out.println("UnionFind4 : " + testUF(uf4, m) + " s");

        UnionFind5 uf5 = new UnionFind5(size);
        System.out.println("UnionFind5 : " + testUF(uf5, m) + " s");

        UnionFind6 uf6 = new UnionFind6(size);
        System.out.println("UnionFind6 : " + testUF(uf6, m) + " s");
    }
}

輸出:遞迴會產生一定的開銷

補充:在方式 5 中也可以變為理想狀態下的情況【不是通過 遞迴 ,通過 迴圈遍歷 來實現】

時間複雜度:O(h) --- 嚴格意思上:O(log  *n)