並查集是一種特別的數據結構,在解決連通性問題屢試不爽。如下代碼均爲java語言的實現java
並查集的做用先整體說一下算法
public void join(int a, int b)
連通a節點和b節點public boolean isJoined(int a, int b)
判斷任意兩個節點是不是連通的來查詢任意節點的根節點,自己你能夠理解並查集是一顆樹,但這顆是反向尋找,並非像一般的樹,自頂向下dfs的,而是一直向上找尋根節點數據結構
private int findP(int x)
查詢x元素的根節點將連通節點的父節點都設置爲相同節點(索引),查詢的時候 f(x)=parent[x],但更新比較麻煩,每次更新須要遍歷全部元素O(n)的複雜度,n是總的元素個數性能
/** * 並查集 快速查詢 */ public class UFQuickFind { int[] parent; public UFQuickFind(int size) { parent = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; } } private int findP(int x) {//查詢操做,實際上是查詢 根節點 if (x < 0 || x >= parent.length) return -1; //或者直接拋出異常 return parent[x]; //直接返回 parent就能夠了,由於全部 鏈接 的節點parent值都相同 } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { for (int i = 0; i < parent.length; i++) { if (parent[i] == ap) parent[i] = bp; //修改爲相同的 parent } } } public boolean isJoined(int a, int b) { //兩個節點是不是 鏈接的 return findP(a) == findP(b); } }
合併的時候,a,b元素,直接讓將a的父親指向b的父親。【通俗一點 a節點的父親 認b節點的根節點作父親了。a節點認祖歸宗了】ui
/** * 快速合併 */ public class UFQuickUnion { int[] parent; public UFQuickUnion(int size) { parent = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; } } private int findP(int x) {//查詢操做,實際上是查詢 根節點 if (x < 0 || x >= parent.length) return -1; //或者直接拋出異常 while (parent[x] != x)//一直搜索到根節點 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { parent[ap] = bp;//讓其中一個節點的根節點 指向 另一個節點的根節點 } } public boolean isJoined(int a, int b) { //兩個節點是不是 鏈接的 return findP(a) == findP(b); } }
因爲快速合併的過程,合併的過程是隨機的,若是全部節點都合併到一塊兒,那麼最後這顆樹可能變成一個鏈表【就是接龍,成爲一條線】,經過節點數來改進,節點數少的接到節點數多的上面,這樣確定不會成一個鏈表code
public class UFNumsUnion { int[] parent; int[] nums;//節點數 public UFNumsUnion(int size) { parent = new int[size]; nums = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; nums[i] = 1; } } private int findP(int x) {//查詢操做,實際上是查詢 根節點 if (x < 0 || x >= parent.length) return -1; //或者直接拋出異常 while (parent[x] != x)//一直搜索到根節點 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { //a節點多,就將 b節點往a節點上合併,這樣的話能夠減小樹的高度 if (nums[ap] > nums[bp]) { parent[bp] = ap; nums[ap] += nums[bp]; //根節點的節點數 增長了 被合併節點的節點數 } else { parent[ap] = bp; nums[bp] += nums[ap]; } } } public boolean isJoined(int a, int b) { //兩個節點是不是 鏈接的 return findP(a) == findP(b); } }
【基於每株節點數的合併】雖然已經挺好的了,但偶爾也會不太合理的狀況以下所示索引
A: o / | \ \ o o o o 5個節點,兩層 B: o / | o o 4個節點 3層 / o
上面這種狀況按照基於節點數的合併,會獲得一個4層的樹(層的深度增長),而更合理的方式是讓A接到B上,這樣最後總體層數保持在3層。層數越小,那麼findP
方法就會變快,而合併也由於調用findP
方法也會加快。get
基於層數的實現,永遠讓層數矮的接到層數高的樹上io
public class UFRankUnion { int[] parent; int[] rank;//樹的層數 int plant; //一共有多少株樹 public UFRankUnion(int size) { plant = size; parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } private int findP(int x) {//查詢操做,實際上是查詢 根節點 if (x < 0 || x >= parent.length) return -1; //或者直接拋出異常 while (parent[x] != x)//一直搜索到根節點 x = parent[x]; return x; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { plant--; //a的層數越高,就將層數少的合併到層數高的上面 if (rank[ap] > rank[bp]) parent[bp] = ap; else if (rank[ap] < rank[bp]) { parent[ap] = bp; } else { //相同狀況的話,隨便就能夠,但整體層高會增長1,畫一個相同層的樹合併一下就知道 parent[ap] = bp; rank[bp]++; } } } public int getPlant() { return plant; } public boolean isJoined(int a, int b) { //兩個節點是不是 鏈接的 return findP(a) == findP(b); } }
咱們設想一下並查集的理想狀態,全部樹都是深度爲1的樹,這種理想情況的findP的時間複雜度爲O(1),大大提升了查詢性能,以下圖所示class
o o / | \ \ / | \ o o o o o o o 全部的樹都是以這種方式去組合的
可是咱們知道,若是A和B合併,層高是否是又變成2層了,如何才能讓層高再次恢復到1層呢。這裏路徑壓縮的一種方式是在查詢的時候,將查詢的節點不斷往上提升,直接接到根節點上
節點1 o / | \ 節點2 o o o / 節點3 o 節點1
查詢節點3的時候,是否是讓節點3接到 節點2的父節點上 parent[節點3]= parent[節點2]
public class UFRankUnionCompressPath { int[] parent; int[] rank;//樹的層數 int plant; //一共有多少株樹 public UFRankUnionCompressPath(int size) { plant = size; parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } private int findP(int x) {//查詢操做,實際上是查詢 根節點 if (x < 0 || x >= parent.length) return -1; //或者直接拋出異常 while (parent[x] != parent[parent[x]]) //若是parent[x]==parent[parent[x]] 說明這顆樹到了第二層,或者第一層 //第一層 由於parent[x]=x因此有 parent[x]==parent[parent[x]] //第二層 由於第二層的parent是根節點 有: parent[x]= root //因此有 parent[parent[x]]= parent[root] 而自己 parent[root]=root { // x = parent[x]; // parent[x] = parent[parent[x]]; //將下層節點往頂層提高,最終 } return parent[x]; } public void join(int a, int b) { int ap = findP(a); int bp = findP(b); if (ap != bp) { plant--; //a的層數越高,就將層數少的合併到層數高的上面 if (rank[ap] > rank[bp]) parent[bp] = ap; else if (rank[ap] < rank[bp]) { parent[ap] = bp; } else { //相同狀況的話,隨便就能夠 parent[ap] = bp; rank[bp]++; } } } public int getPlant() { return plant; } public boolean isJoined(int a, int b) { //兩個節點是不是 鏈接的 return findP(a) == findP(b); } }
期待下一篇,並查集運用相關的算法題及題解