問題:問題的輸入是一列整數對,其中每一個整數都表示一個某種類型的對象,一對整數p q能夠被理解爲「p和q是相連的」。java
當程序從輸入中讀取了整數對p q時,若是已知的全部整數對都不能說明p和q是相連的,那麼則將這一對整數寫入到輸出中;若是已知數據能夠說明p和q是相連的,那麼程序應該忽略p q這對整數並繼續處理輸入中的下一對整數。咱們將這個問題通俗地叫作動態連通性問題。算法
應用以下:輸入的整數表示的多是一個大型計算機網絡中的計算機,而整數對則表示網絡中的鏈接。這個程序可以判斷咱們是夠須要在p和q之間架設一條新的鏈接才能進行通訊,或是咱們能夠經過已有的鏈接在二者之間創建通訊線路。數組
union-find算法的API網絡
public class UF | |
UF (int N) | 以整數標誌(0到N-1)初始化N個觸點 |
void union (int p,int q) | 在p和q之間創建一條鏈接 |
int find (int p) | p(0到N-1)所在份量的標識符 |
boolean connected (int p,int q) | 若是p和q存在於同一個份量中則返回true |
int count() | 連通份量的數量 |
咱們將討論三種不一樣的實現,他們均根據以觸點爲索引的id[]數組來肯定兩個觸點是否存在於相同的連通份量中。數據結構
1. quick-find算法ui
保證當且僅當id[p]等於id[q]時p和q是連通的,換句話說,在同一個連通份量中的全部觸點在id[]中的值必須所有相同。算法以下:spa
public int find(int p){ return id[p]; } public void union(int p,int q) { //將p和q歸併到相同份量中 int pID=find(p); int qID=find(q); //若是p和q已經在相同的份量之中則不須要採起任何行動 if(pID==qID) return; //將p和份量重命名爲q的名稱 for(int i=0;i<id.length;i++) if(id[i]==pID)id[i]=qID; count--; }
分析:find()操做的速度顯然是很快的,覺得他只須要訪問id[]數組一次。。可是quick-find算法通常沒法處理大型問題,由於每一對輸入union()都須要掃描整個id[]數組。計算機網絡
每次find()調用都只須要訪問一次數組,而歸併兩個份量的union()操做訪問數組的次數在(N+3)到(2N+1)之間。code
假設咱們使用quick-find算法解決動態連通性問題,而且最後只獲得了一個連通份量,那麼至少須要調用N-1次union(),即至少(N+3)*(N-1)~N²次數訪問,於是能夠得出結論,quick-find算法的運行時間對於最終只能獲得少數連通份量的通常應用是平方級別的。對象
2. quick-union算法
此算法重點是提升union()方法的速度,它和quick-find算法是互補的。
定義數據結構時,咱們須要每一個觸點所對應的id[]元素都是同一個份量中的另外一個觸點的名稱(也多是它本身)——咱們將這種聯繫稱爲連接。
在實現find()方法時,咱們從給定的觸點開始,由他的連接獲得另外一個觸點,再由這個觸點連接到第三個觸點,如此繼續跟隨連接直到到達一個根觸點。不一樣觸點,通過一系列的連接,若是能夠到達同一個根觸點,則說明這兩個觸點存在於同一個連通份量中。
使用森林的概念,根觸點做爲根節點,quick-union算法更容易讓人理解。算法以下:
public int find(int p){ //找出份量的名稱,即根觸點的名稱 while(p!=id[p])p=id[p];//存儲的連接不等於自己,則繼續追溯下一個觸點 return p; } public void union(int p,int q) { //將p和q歸併到相同份量中,即將p和q的根觸點統一 int pRoot=find(p); int qRoot=find(q); //若是p和q已經在相同的份量之中則不須要採起任何行動 if(pRoot==qRoot) return; //將p的根觸點,指向q的根觸點 id[pRoot]=qRoot; count--; }
分析:quick-union算法看起來比quick-find算法更快,可是分析它的算法成本難度很大,由於這依賴於輸入的特色。在最好狀況下,find()只須要訪問一次數組就能獲得一個觸點所在的份量標識符;而在最壞狀況下,這須要2N+1次數組訪問。不可貴出,quick-union算法在構造有一個最佳狀況輸入使得解決動態連通性的問題的用例的運行時間是線性級別(1*N)的;而最壞狀況下,他的運行時間是平方級別(N*(2N+1))的。
3.加權quick-union算法
quick-union算法能夠看作quick-find算法的一種改良,由於它解決了quick-find算法中最主要的問題(union()操做老是線性級別的,由於每次都須要遍歷整個數組來改掉某個連通份量內的值)。
可是quick-union算法仍存在問題,咱們不能保證任何狀況下它都能比quick-find算法快的多。由於以前提到quick-union算法利用到森林,樹的概念,每次find()都須要層層遍歷到根節點,所以運行時間和節點在書中的深度息息相關。所以咱們須要一個改進的方法減少節點的深度。
加權quick-union算法就能實現這樣的改進,由於他老是能讓較小的樹鏈接到較大的樹上。固然,這須要咱們對數據結構進行相應的改進,即添加實例變量來記錄每一棵樹的大小,即份量大小。算法以下:
public class WeightedQuickUnionUF { private int[] id; //父連接數組(由觸點索引) private int[] sz; //各根節點所對應的份量大小 private int count; //連通份量的數量 public WeightedQuickUnionUF(int N) { count=N; id=new int[N]; for(int i=0;i<N;i++)id[i]=i; sz=new int[N]; for(int i=0;i<N;i++)id[i]=1; } public int count() { return count; } public boolean connected(int p,int q) { return find(p)==find(q); } public int find(int p) { while(p!=id[p])p=id[p]; return p; } public void union(int p,int q) { int i=find(p); int j=find(q); if(i==j)return; //將小樹的根節點鏈接到大樹的根節點 if (sz[i]<sz[j]) { id[i]=j; sz[j]+=sz[i]; } else { id[j]=i; sz[i]+=sz[j]; } count--; } }
對於動態連通性問題,加權quick-union算法是三種算法中惟一可以用於解決大型實際問題的算法。
最優算法
先說結論,路徑壓縮的加權quick-union算法是最優算法。
路徑壓縮即在檢查每一個節點的同時將他們直接連接到根節點,也就是實現了樹的幾乎徹底的扁平化,這和quick-find算法理想狀況下所獲得的樹很是接近。