優雅的數據結構–並查集

概念介紹


先來想一想「親戚」這個詞的定義:「指和本身有血親和姻親的人」。你和你女朋友家眷自己並不是是親戚關係,一旦結婚後,兩家人便成爲了一家人,你的家人包括你在內和你女朋友及其家人自動成爲了親戚,這就是一個典型的並查集應用。並查集是一種樹形的數據結構,用於處理一些不相交集合的合併及查詢,上面例子中「結婚」其實就是並查集的合併操做

下面咱們來演示下並查集的常規操做,咱們默認建立6個元素,這6個元素咱們能夠當作是互不相交的6個集合node



進行幾回簡單合併操做,咱們把元素0,2,4合併爲集合set0,1,3合併爲set1,5單獨當作一個set2


查詢操做


實現方法


初始化(make_set)

咱們能夠把並查集當作是由不少顆樹組成的森林,每棵樹中相連的結點都表明屬於同一集合,樹中parent指向本身的根結點被視爲該集合的表明。初始的時候,咱們用一個parent數組存儲全部結點的父結點下標,因爲默認狀況下每一個集合互不相交,因此咱們令每一個結點的parent都指向本身,這樣就生成了N棵以本身爲根的樹組成的森林。ios


parent數組的初始化結構以下圖所示:
git


初始化並查集的代碼:github

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。bash



合併2和4,需將4指向2的根結點0。


合併1和3,需將3指向1。


接着,若是想要繼續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的代碼:ui

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]);// 輸出全部指向
    }
}
複製代碼

參考文章


Union-Find Algorithms
維基百科

推薦題目

PAT- 1118 Birds in Forest
PAT- 1118 Birds in Forest--代碼


本篇已同步到我的博客:優雅的數據結構–並查集

相關文章
相關標籤/搜索