概念介紹
先來想想「親戚」這個詞的定義:「指和自己有血親和姻親的人」。你和你女友家屬本身並非是親戚關係,一旦結婚後,兩家人便成為了一家人,你的家人包括你在內和你女友及其家人自動成為了親戚,這就是一個典型的並查集應用。並查集是一種樹形的資料結構,用於處理一些不相交集合的合併及查詢,上面例子中「結婚」其實就是並查集的合併操作
下面我們來演示下並查集的常規操作,我們預設建立6個元素,這6個元素我們可以看成是互不相交的6個集合
實現方法
初始化(make_set)
我們可以把並查集看成是由很多顆樹組成的森林,每棵樹中相連的結點都代表屬於同一集合,樹中parent指向自己的根結點被視為該集合的代表。初始的時候,我們用一個parent陣列儲存所有結點的父結點下標,由於預設情況下每個集合互不相交,所以我們令每個結點的parent都指向自己,這樣就生成了N棵以自己為根的樹組成的森林。
parent陣列的初始化結構如下圖所示:
初始化並查集的程式碼:
void make_set (){
for (int i=0;i<N;i++){
parent[i] = i;// 如上圖i的parent指向自己
}
}
複製程式碼
合併(Union)
Union(a,b)會將a所在的集合與b所在的集合相結合。在資料結構的實現上,只需要將b的根結點指向a的根結點,或a的根結點指向b的根結點即可,本文中預設使用前者。假設我們現在要將0,2,4合併為一個集合,1,3 合併為一個集合,5單獨視為一個集合,那麼運算的過程的可能如下:
合併0和2,需將2指向0。
接著,如果想要繼續Union(5,3),我們可以先獲得結點3所處樹的根結點1,讓1指向5即可。但是這樣樹的高度要比5指向1的樹要高,隨著並查集規模的增大,樹會多出很多不必要的高度,這將導致並查集的查詢更耗時。
為了讓合併後樹的整體高度相對更矮,在每次合併時,我們讓高度較矮的樹併入高度較高的樹,這種優化會在之後的程式碼中體現出來。
最後,如果我們想要Union(2,3),由於2,3各自所處的樹高度相同,所以按預設方式將「3」的根結點「1」指向「2」的根結點「0」即可。
在實現Union函式之前,我們先增加一個rank[N]陣列記錄高度,預設的時候rank陣列全部設定為0,rank中數值隨著並查集的合併而改變。下面給出Union的程式碼:
void union_set(int a,int b) {
if (a==b) return; // 相同
int root_a = find_root(a);//找到a的根結點
int root_b = find_root(b);//找到b的根結點
if (root_a == root_b) return; //根結點相同
int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 選出較高的樹
int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 選出較低的樹
if( higher_root == lower_root ) {
// 兩顆樹高度相等的情況
parent[root_b] = root_a; //root_b.parent 指向 root_a (預設操作)
++rank[root_a];// 高度+1
}else {
parent[lower_root] = higher_root; // 較矮的樹指向較高的樹,不會改變整體高度
}
}
複製程式碼
查詢(Find)
查詢某個元素所在的集合非常簡單,由於parent陣列記錄了每一個元素的父結點,我們只需要遞迴回溯即可。
執行find_root(5)
後沿著紅線向上回溯找到0,執行find_root(2)
後沿著紅線向上回溯也找到了0,說明5和2同屬一個集合,而執行find_root(7)
後沿著紅線回溯找到了6,故7和元素5,2不屬於同一個集合。下面給出實現程式碼:
int find_root(int node) {
if (parent[node] == node) return node;
return find_root(parent[node]);
}
複製程式碼
路徑壓縮(Path Compression)
在查詢某個元素的所在集合的時候,上面的find_root(int node)
函式會返回元素所在的樹的根結點------這個集合的代表,在這個過程中,我們可以將當前待查詢的元素直接指向這個根結點,降低樹的高度,從而使得查詢速度得到提升。以上圖為例子,執行find_root(3)
,find_root(5)
後樹形結構會變成如下結構:
程式碼實現上的改動非常小:
int find_root(int node) {
if (parent[node] == node) return node;
parent[node] = find_root(parent[node]); // 指向根結點
return parent[node];
}
複製程式碼
完整的程式碼+前面的例子
並查集的程式碼和邏輯都非常精簡,在我看來是非常優雅的資料結構。
#include<iostream>
#include <stdio.h>
#define N 10000+10
int parent[N];
int rank[N];
void make_set (){
for (int i=0;i<N;i++){
parent[i] = i;
rank[i] = 0;
}
}
int find_root(int node) {
if (parent[node] == node) return node;
parent[node] = find_root(parent[node]);
return parent[node];
}
void union_set(int a,int b) {
if (a==b) return; // 相同
int root_a = find_root(a);//找到a的根結點
int root_b = find_root(b);//找到b的根結點
if (root_a == root_b) return; //根結點相同
int higher_root = rank[root_a]>rank[root_b] ?root_a:root_b;// 選出較高的樹
int lower_root = rank[root_a]<rank[root_b]?root_a:root_b;// 選出較低的樹
if( higher_root == lower_root ) {
// 兩顆樹高度相等的情況
parent[root_b] = root_a; //root_b.parent 指向 root_a (預設操作)
++rank[root_a];// 高度+1
}else {
parent[lower_root] = higher_root; // 較矮的樹指向較高的樹,不會改變整體高度
}
}
int main(){
make_set();
union_set(0, 2);
union_set(2, 4);
union_set(1, 3);
union_set(5, 3);
union_set(2, 3);
find_root(3);
find_root(5);
for(int i=0;i<6;i++) {
printf("%d ",parent[i]);// 輸出所有指向
}
}
複製程式碼
參考文章
推薦題目
PAT- 1118 Birds in Forest
PAT- 1118 Birds in Forest--程式碼
本篇已同步到個人部落格:優雅的資料結構–並查集