簡介
適用於: 可以解決連接問題, 查看網絡中的節點的連接狀態(比通過求兩個網絡的路徑來看連接狀態效率高); 求兩個集合的並集
合併和查找
指向同一個根節點的節點在同一個集合
並查集和其他樹相比比較特殊的一點是: 其他的樹都是從父節點指向子節點, 並查集是子節點指向父節點
一開始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(logn)比O(log n)快, 但比O(1)慢, 近乎是O(1)級別的