並查集的理解與實現總結
並查集的應用十分廣泛,包括一些演算法,當應用上並查集的時候,也會更容易實現。下面總結下並查集的相關內容。
什麼是並查集?
個人的理解是:並查集就是對集合三種常用操作的再一次抽象。分別是集合的合併(Union)、元素的搜尋(Find)和對集合的分解。因為這3中操作非常常用並且又不囿於集合,所以就把這一組操作抽象成一個獨立的資料結構。
標準定義
在一些應用問題中,需要將n個不同的元素劃分成一組不相交的集合。開始時每個元素自成一個單元素集合,然後按照一定規律將歸於同一組元素的集合合併,在此過程中需要反覆查詢某個元素歸屬於哪個集合的運算,適合於描述這類問題的抽象資料型別稱為並查集(union-find set)。
並查集的3種操作!
從上面的定義也可以看出來,並查集的三種操作是:
(1)Union(Root1, Root2):把子集合Root2併入集合Root1中。要求這兩個集合互不相交,否則不執行合併。
(2)Find(x):搜尋單元素x所在的集合,並返回該集合的名字。
(3)UnionFindSets(s):建構函式,將並查集中s個元素初始化為s個只有一個單元素的子集合。
並查集的一種實現方案
實現並查集的方式有多種,這裡主要總結用**樹結構(父指標表示法)**來實現並查集及其相關操作。
用這種實現方式,每個集合用一棵樹表示,樹的每一個節點代表集合的一個單元素。所有各個集合的全集合構成一個森林,並用樹與森林的父指標表示法來實現。其下標代表元素名。第I個陣列元素代表包含集合元素I的樹節點。樹的根節點的下標代表集合名,根節點的父為-1,表示集合中元素個數。
下面看一個例子:
全集合是S = {0,1,2,3,4,5,6,7,8,9},初始化每個元素自成為一個單元素子集合。(書上原圖,感覺挺清晰的)
經過一段時間的計算,這些子集合併成3個集合,他們是全集合S的子集合:S1 = {0,6,7,8},S2= {1,4,9},S3 = {2,3,5}。則表示他們並查集的樹形結構如下圖:
上面陣列中的元素值有兩種含義:
(1)負數表示當前節點是樹的根節點,負數的絕對值表示樹中節點的個數,也即集合中元素的個數。
(2)正數表示其所屬的樹的根節點,由樹形表示很容易理解,這也是樹的父指標表示的定義。
經過上面對相關資料的組織,再回頭來看並查集的3中核心操作是怎樣依託於樹來實現的:
(1)將root2併入到root1中,其實就可以直接把root2的陣列元素(就是他的父節點)改成root1的名字(就是他所在的陣列下標)。
下面的圖表示了合併兩個子集合的過程:
(2)查詢x所屬於的根節點(或者說是x所屬於的集合),就可以一直找array[x],直到array[x]小於0,則證明找到了根(所在集合)。
下面的圖示意了查詢一個節點所屬集合的過程:
(3)將整個集合初始化為單元素集合,其實就是建立樹的父指標陣列的過程,把陣列元素全初始化為-1,也就表示了每個元素都各佔一個集合。
有了上面的理論,程式碼也比較容易實現出來!下面給出了一個程式碼的例項:
/*
*樹結構構建並查集,其中樹用父指標形式表示
*/
#include <iostream>
const int DefaultSize = 10;
class UFSets { //集合中的各個子集合互不相交
public:
UFSets(int sz = DefaultSize); //建構函式 (並查集的基本操作)
~UFSets() { delete[] parent; } //解構函式
UFSets& operator = (UFSets& R); //過載函式:集合賦值
void Union(int Root1, int Root2); //兩個子集合合併 (並查集的基本操作)
int Find(int x); //搜尋x所在集合 (並查集的基本操作)
void WeightedUnion(int Root1, int Root2); //加權的合併演算法
private:
int *parent; //集合元素陣列(父指標陣列)
int size; //集合元素的數目
};
UFSets::UFSets(int sz) {
//建構函式,sz是集合元素的個數,父指標陣列的範圍0到sz-1
size = sz; //集合元素的個數
parent = new int[size]; //開闢父指標陣列
for (int i = 0; i < size; i ++) { //初始化父指標陣列
parent[i] = -1; //每個自成單元素集合
}
}
int UFSets::Find(int x) {
//函式搜尋並返回包含元素x的樹的根
while (parent[x] >= 0) {
x = parent[x];
}
return x;
}
void UFSets::Union(int Root1, int Root2) {
//函式求兩個不相交集合的並,要求Root1與Root2是不同的,且表示了子集合的名字
parent[Root1] += parent[Root2]; //更新Root1的元素個數
parent[Root2] = Root1; //令Root1作為Root2的父節點
}
void UFSets::WeightedUnion(int Root1, int Root2) {
//使用節點個數探查方法求兩個UFSets集合的並
int r1 = Find(Root1); //找到root1集合的根
int r2 = Find(Root2); //找到root2集合的根
if (r1 != r2) { //兩個集合不屬於同一樹
int temp = parent[r1] + parent[r2]; //計算總節點數
if (parent[r2] < parent[r1]) { //注意比較的是負數,越小元素越多,此處是r2元素多
parent[r1] = r2; //r1作為r2的孩子
parent[r2] = temp; //更新r2的節點個數
}
else {
parent[r2] = r1; //...
parent[r1] = temp; //...
}
}
}
程式碼的註釋比較詳盡,我就不在贅言。但是有一個注意點我已經寫在了下面!
當前並查集的改進!
的確,有一個極端的狀況使得上面的樹實現的並查集效能低下!問題原因在於,這裡沒有規定子集合並的順序,更確切的說是子集一直在向同一個方向依附:
下面的圖片展示了當Union(0,1),Union(1,2),Union(2,3),Union(3,4)執行完後的樹的形狀。
在這種極端情況下他編變成了一個單連結串列(退化的樹),這樣的話,用Find函式查詢完所有的節點所歸屬的集合將會開銷的時間複雜度為:O(n^2)。
怎樣來改變這種狀況,就是在合併兩個子集的時候,規定順序,程式碼中是讓元素多的始終作為父節點,這樣就避免了這種麻煩。
另外還可以用效能更加的查詢演算法,例如摺疊規則壓縮路徑。並查集的一個重要的應用是在圖論中生成最小生成樹的Kruskal演算法,充分體現了並查集的優越性和思想,之後會寫相關的博文總結此演算法!
參考書目:《資料結構 C++語言描述 第二版》 殷人昆著
想一起解決程式設計中遇到的麻煩嗎?想一起學習更多軟體知識嗎?想找一群志同道合的朋友嗎?想找到自己關於計算機真正的興趣所在嗎?那就加入我們吧!(老學長公眾號剛剛開通不久,每隔3天會發表一篇有質量的文章,希望大家多支援!)
相關文章
- 並查集(一)並查集的幾種實現並查集
- 並查集(UnionFind)技巧總結並查集
- 並查集應用總結並查集
- 並查集的概念與演算法實現並查集演算法
- 並查集java實現並查集Java
- 並查集-Java實現並查集Java
- 並查集的使用及其實現並查集
- 資料結構與演算法知識點總結(3)樹、圖與並查集資料結構演算法並查集
- 資料結構 — 並查集的原理與應用資料結構並查集
- 資料結構-並查集資料結構並查集
- 【並查集】【帶偏移的並查集】食物鏈並查集
- 基於並查集的六度分隔理論的驗證與實現並查集
- 演算法與資料結構之並查集演算法資料結構並查集
- 並查集到帶權並查集並查集
- 簡單易懂的並查集演算法以及並查集實戰演練並查集演算法
- 資料結構之並查集資料結構並查集
- 優雅的資料結構–並查集資料結構並查集
- 並查集的使用並查集
- 並查集詳解與應用並查集
- GRPC與 ProtoBuf 的理解與總結RPC
- Promise 小小的總結與實現Promise
- 【資料結構】帶權並查集資料結構並查集
- 資料結構:速通並查集資料結構並查集
- 並查集(二)並查集的演算法應用案例上並查集演算法
- 並查集的應用並查集
- 3.1並查集並查集
- 並查集(小白)並查集
- Mybatis 查詢語句結果集總結MyBatis
- JS實現並集,交集和差集JS
- 二分查詢實現----面試總結面試
- 跨域方案總結與實現跨域
- BST查詢結構與折半查詢方法的實現與實驗比較
- 並查集演算法Union-Find的思想、實現以及應用並查集演算法
- URLEncode與URLDecode總結與實現
- 關於react我的理解與總結React
- Java常量池理解與總結Java
- 並查集的應用2並查集
- (轉載)並查集的作用並查集