並查集 Union Find 路徑壓縮 簡介+實現

簡介

適用於: 可以解決連接問題, 查看網絡中的節點的連接狀態(比通過求兩個網絡的路徑來看連接狀態效率高); 求兩個集合的並集
在這裏插入圖片描述

合併和查找

指向同一個根節點的節點在同一個集合

並查集和其他樹相比比較特殊的一點是: 其他的樹都是從父節點指向子節點, 並查集是子節點指向父節點

一開始5個節點都自成一個集合, 他們的父節點是自己
在這裏插入圖片描述
如果合併1, 2 (執行union(1, 2)), 就把節點2作爲節點1的父節點, 節點1指向節點2
在這裏插入圖片描述
合併節點3, 4(union(3, 4), 之後再合併節點4, 5(union(4, 5)
要尋找節點3的根節點只需要while循環一直往上查找, 直到一個節點的父節點是他自己(下圖的節點5), 這個節點就是節點3的父節點
同理節點4的根節點也是節點5, 所以節點3, 4在同一個集合
在這裏插入圖片描述
在上面這種情況下分別嘗試, 合併節點5, 2(union(5, 2)) 和 合併節點2, 5(union(2, 5))
情況一: 合併節點5, 2(union(5, 2)), 節點2是5的父親
在這裏插入圖片描述
情況二: 合併節點2, 5(union(2, 5)), 節點5是2的父親
在這裏插入圖片描述
可以看出情況一的樹的深度比情況二的要深, 極端的情況下甚至可能退化成一個鏈表, 複雜度將變成O(n), 這是我們不希望看到的, 所以在進行合併的時候要做一些判斷, 把深度較高的樹的根節點作爲深度較低的樹的根節點的父親

這裏給每個節點引入一個rank值, 表示以這個節點爲根的樹的深度值

// p節點所在的樹的元素個數比較少
// 根據兩個元素所在樹的rank不同判斷合併方向
if(rank[pRoot] < rank[qRoot])  // 深度低的樹的根節點把深度高的樹的根節點作爲父親
    parent[pRoot] = qRoot;  // qRoot所在樹的深度沒變, rank不用維護
else if(rank[pRoot] > rank[qRoot])
    parent[qRoot] = pRoot;
else{  // rank[pRoot] == rank[qRoot]
    parent[pRoot] = qRoot;  // 如果棵樹的深度一樣, 可以任意選擇一個根作爲父親
    rank[qRoot] += 1;  // 但是作爲父親的那個節點的rank值會變化, 因爲深度變了
}

路徑壓縮

在這裏插入圖片描述
回到一開始, 如果我們合併節點1, 2(union(1, 2)), 然後合併節點2, 3(union(2, 3))
在這裏插入圖片描述
再合併節點3, 4(union(3, 4)), 最後合併節點4, 5(union(4, 5))
在這裏插入圖片描述
按照這種順序合併的話, rank的引入也沒用, 我們依然得到了一條鏈表
因此我們需要請出路徑壓縮來解決這個問題

非遞歸

在這裏插入圖片描述
在上面這種情況下, 如果要合併節點1, 4(union(1, 4), 合併前我們都要先看兩個節點是的根節點是否一樣, 不一樣才合併, 在向上找節點1的父親時, 我們可以順便修改節點1的父親, 壓縮這棵樹的深度

parent[節點1] = parent[parent[節點1]];  // 節點1的父親等於原來他父親(節點2)的父親(節點3)

結果如下
在這裏插入圖片描述
再執行union(3, 6)的結果如下, 查找節點6的父親時進行壓縮
在這裏插入圖片描述
代碼是這樣的

while(節點 != parent[節點])  // 直到找到根節點才停止
    parent[節點] = parent[parent[節點];  // 路徑壓縮
    節點 = parent[節點];  // 向上查找, 移動

遞歸

在下面這種情況, 使用非遞歸的路徑壓縮
在這裏插入圖片描述
首先節點7在查找父親的時候順便修改了父親
在這裏插入圖片描述
然後union(7, 2)得到的結果如下圖
在這裏插入圖片描述
但是並查集並沒有限制孩子的個數, 所以我們可以極端一些, 在節點7在查找父親的時候把右邊那棵樹壓縮成如下圖
在這裏插入圖片描述
這就要使用遞歸了
代碼如下

find(節點)
    if(節點!= parent[節點])
        parent[節點] = find(parent[節點]);
    return parent[節點];

在這裏插入圖片描述
但是遞歸是有額外開銷的, 實際運行起來並不會比非遞歸的實現快很多
而實際上非遞歸的實現如果查找的次數足夠多的話, 修改父親的次數足夠多的話也能夠得到跟使用遞歸一樣的樹結構, 而且更快

實現

// 基於rank(樹的深度)的優化
// 路徑壓縮
public class UnionFind implements UF {
    private int[] parent;
    // 如果沒有路徑壓縮的話, rank[i]表示以i爲根的集合所表示的樹的深度
    // 但rank的值在路徑壓縮的過程中, 有可能不在是樹的深度值
    // 這也是rank不叫height或者depth的原因, 他只是作爲比較的一個標準
    private int[] rank;


    public UnionFind(int size){
        parent = new int[size];
        rank = new int[size];

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

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

    /**
     * 查找元素p所對應的集合編號
     * O(h)複雜度, h爲樹的高度
     * @param p
     * @return
     */
    private int find(int p){
        if(p < 0 && p >= parent.length)
            throw new IllegalArgumentException("Out of bound. ");

        // 向上查找, 直到根節點
        while(p != parent[p]){
            parent[p] = parent[parent[p]];  // 路徑壓縮
            p = parent[p];
        }
        return p;
    }

	/**
     * 查找元素p所對應的集合編號, 遞歸實現
     * @param p
     * @return
     */
    private int findR(int p){
        if(p < 0 && p >= parent.length)
            throw new IllegalArgumentException("Out of bound. ");

        if(p != parent[p])
            parent[p] = findR(parent[p]);  // 路徑壓縮
        return parent[p];
    }

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

    /**
     * 合併p, q所屬的集合
     * 將rank低的集合合併到rank高的集合上
     * O(h)
     * @param p
     * @param q
     */
    @Override
    public void unionElements(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);

        if(pRoot == qRoot)
            return;

        // 根據兩個元素所在樹的rank不同判斷合併方向
        if(rank[pRoot] < rank[qRoot]){
            parent[pRoot] = qRoot;  // qRoot所在樹的深度沒變, rank不用維護
        }
        else if(rank[pRoot] > rank[qRoot]){
            parent[qRoot] = pRoot;
        }
        else{  // rank[pRoot] == rank[qRoot]
            parent[pRoot] = qRoot;
            rank[qRoot] += 1;
        }
    }
}

時間複雜度嚴格意義上不是O(h), 而是O(logn), iterated logarithm
在這裏插入圖片描述
O(log
n)比O(log n)快, 但比O(1)慢, 近乎是O(1)級別的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章