在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題。有一個聯合-查找算法(union-find algorithm)定義了兩個用於此數據結構的操做:php
因爲支持這兩種操做,一個不相交集也常被稱爲聯合-查找數據結構(union-find data structure)或合併-查找集合(merge-find set)。其餘的重要方法,MakeSet,用於創建單元素集合。有了這些方法,許多經典的劃分問題能夠被解決。java
爲了更加精確的定義這些方法,須要定義如何表示集合。一種經常使用的策略是爲每一個集合選定一個固定的元素,稱爲表明,以表示整個集合。接着,Find(x) 返回 x 所屬集合的表明,而 Union 使用兩個集合的表明做爲參數。算法
上圖中簡單演示了並查集的兩個操做,一個是FIND,一個UNION。數組
並查集森林是一種將每個集合以樹表示的數據結構,如上圖所示。其中每個節點保存着到它的父節點的引用(見意大利麪條堆棧)。這個數據結構最先由Bernard A. Galler和Michael J. Fischer於1964年提出,[1]可是通過了數年才完成了精確的分析。數據結構
在並查集森林中,每一個集合的表明便是集合的根節點。「查找」根據其父節點的引用向根行進直到到底樹根。「聯合」將兩棵樹合併到一塊兒,這經過將一棵樹的根鏈接到另外一棵樹的根。實現這樣操做的一種方法是函數
function MakeSet(x) x.parent := x function Find(x) if x.parent == x return x else return Find(x.parent) function Union(x, y) xRoot := Find(x) yRoot := Find(y) xRoot.parent := yRoot
這是並查集森林的最基礎的表示方法,這個方法不會比鏈表法好,這是由於建立的樹可能會嚴重不平衡;然而,能夠用兩種辦法優化。優化
第一種方法,稱爲「按秩合併」,即老是將更小的樹鏈接至更大的樹上。由於影響運行時間的是樹的深度,更小的樹添加到更深的樹的根上將不會增長秩除非它們的秩相同。在這個算法中,術語「秩」替代了「深度」,由於同時應用了路徑壓縮時(見下文)秩將不會與高度相同。單元素的樹的秩定義爲0,當兩棵秩同爲r的樹聯合時,它們的秩r+1。只使用這個方法將使最壞的運行時間提升至每一個MakeSet、Union或Find操做、。spa
優化後的MakeSet
和Union
僞代碼:3d
function MakeSet(x) x.parent := x x.rank := 0 function Union(x, y) xRoot := Find(x) yRoot := Find(y) if xRoot == yRoot return // x和y不在同一個集合,合併它們。 if xRoot.rank < yRoot.rank xRoot.parent := yRoot else if xRoot.rank > yRoot.rank yRoot.parent := xRoot else yRoot.parent := xRoot xRoot.rank := xRoot.rank + 1
第二個優化,稱爲「路徑壓縮」,是一種在執行「查找」時扁平化樹結構的方法。關鍵在於在路徑上的每一個節點均可以直接鏈接到根上;他們都有一樣的表示方法。爲了達到這樣的效果,Find
遞歸地通過樹,改變每個節點的引用到根節點。獲得的樹將更加扁平,爲之後直接或者間接引用節點的操做加速。code
這兒是Find
:
function Find(x) if x.parent != x x.parent := Find(x.parent) return x.parent
這兩種方法的優點互補,同時使用兩者的程序每一個操做的平均時間僅爲,
是
的反函數,其中
是急速增長的阿克曼函數。由於
是其的反函數,故
在
十分巨大時仍是小於5。所以,平均運行時間是一個極小的常數。
實際上,這是漸近最優算法:Fredman和Saks在1989年解釋了的平均時間內能夠得到任何並查集。
package search; public class UnionFindSet { private int[] parents_; private int[] ranks_; public UnionFindSet(int n) { parents_ = new int[n+1]; ranks_ = new int[n+1]; for(int i=0;i<=n;i++) { parents_[i]=i; ranks_[i]=i; } } public boolean Union(int u,int v) { int pu = Find(u); int pv = Find(v); if(pu==pv) return false; if (ranks_[pv] > ranks_[pu]) parents_[pu] = pv; else if (ranks_[pu] > ranks_[pv]) parents_[pv] = pu; else { parents_[pv] = pu; ranks_[pu] += 1; } return true; } public int Find(int u) { while (parents_[u]!=u) { parents_[u]=parents_[parents_[u]]; u=parents_[u]; } return u; } }
操做很簡單:先設置一個數組(陣列)Father[x],表示x的「父親」的編號。 那麼,合併兩個不相交集合的方法就是,找到其中一個集合最父親的父親(也就是最久遠的祖先),將另一個集合的最久遠的祖先的父親指向它。
void Union(int x,int y) { fx = getfather(x); fy = getfather(y); if(fy!=fx) father[fx]=fy; }
仍然使用上面的數組。則本操做便可轉換爲尋找兩個元素的最久遠祖先是否相同。尋找祖先能夠採用遞歸實現,見後面的路徑壓縮算法。
bool same(int x,int y) { return getfather(x)==getfather(y); } /*返回true 表示相同根結點,返回false不相同*/