並查集是一種比較獨特的數據結構,它是由孩子節點指向父親節點的樹形結構。並查集能高效地解決鏈接問題(Connectivity Problem)java
說明:上圖就是一個鏈接問題,左上角兩個點直接是否被鏈接,肉眼觀察就能給出確定的答案,若是問左上角的一點與右下角的一點是不是鏈接的,用肉眼就很難觀察了。並查集正是高效解決這一問題的數據結構。segmentfault
/** * 基於數組模擬並查集初版(Quick Find) * 查詢操做的時間複雜度是:O(1) * 合併操做的時間複雜度是:O(n) */ public class UnionFind1 implements UF { private int[] id; public UnionFind1(int size) { id = new int[size]; for (int i = 0; i < id.length; i++) { id[i] = i; } } @Override public int getSize() { return id.length; } /** * 查找元素p所對應的集合編號 * @param p 元素ID * @return */ private int find(int p) { if (p < 0 && p >= id.length) { throw new IllegalArgumentException("p is out of bound"); } return id[p]; } /** * 查看元素p和元素q是否所屬同一個集合 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } /** * 合併元素p和元素q所屬的集合 * @param p 元素ID * @param q 元素ID */ @Override public void unionElements(int p, int q) { int pID = find(p); int qID = find(q); if (pID == qID) { return; } //將全部屬於p元素所在的集合編號覆蓋爲q元素所屬的集合編號 for (int i = 0; i < id.length; i++) { if (id[i] == pID) { id[i] = qID; } } } }
說明:以上的實現方式基於數組實現,數組中值不一樣則表示元素屬於不一樣的集合;查詢操做的時間複雜度是O(1),union操做的時間複雜度是O(n),由於須要遍歷數組中各個元素,判斷是否須要修改集合序號。數組
/** * 基於數組模擬並查集第二版 */ public class UnionFind2 implements UF { private int[] parent; public UnionFind2(int size) { parent = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; } } @Override public int getSize() { return parent.length; } /** * 查找元素p所對應的根節點集合編號 * 時間複雜度爲O(h),h爲樹的高度 * @param p * @return */ private int find(int p) { if (p < 0 && p >= parent.length) { throw new IllegalArgumentException("p is out of bound"); } while (p != parent[p]) { p = parent[p]; } return p; } /** * 查看元素p和元素q是否所屬同一個集合 * 時間複雜度爲O(h),h爲樹的高度 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } parent[pRoot] = qRoot; } }
說明:Quick Union是基於數組的樹結構,查詢和合並操做的時間複雜度是O(h),跟樹的深度相關。相比Quick Find方式犧牲了點查詢性能,可是合併性能獲得了提高。數據結構
/** * 基於數組模擬並查集第三版 * 基於size元素個數對並查集合並操做進行優化 */ public class UnionFind3 implements UF { private int[] parent; /** * sz[i]表示以i爲根的集合中元素個數 */ private int[] sz; public UnionFind3(int size) { parent = new int[size]; sz = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; sz[i] = 1; } } @Override public int getSize() { return parent.length; } /** * 查找元素p所對應的根節點集合編號 * 時間複雜度爲O(h),h爲樹的高度 * @param p * @return */ private int find(int p) { if (p < 0 && p >= parent.length) { throw new IllegalArgumentException("p is out of bound"); } while (p != parent[p]) { p = parent[p]; } return p; } /** * 查看元素p和元素q是否所屬同一個集合 * 時間複雜度爲O(h),h爲樹的高度 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } //根據兩個元素所在書的元素個數不一樣判斷合併方向 //將元素個數少的集合合併到元素個數多的集合上 if (sz[pRoot] < sz[qRoot]) { parent[pRoot] = qRoot; sz[qRoot] += sz[pRoot]; } else { parent[qRoot] = pRoot; sz[pRoot] += sz[qRoot]; } } }
說明:在Quick Union的基礎上,爲了不樹的深度太大,極端狀況下樹會退化成鏈表形式,因此考慮了樹的size,小集合樹指向大集合樹,而避免了大集合樹指向小集合樹,有效避免了集合樹深度過大問題。dom
/** * 基於數組模擬並查集第四版 * 基於rank深度對並查集合並操做進行優化 */ public class UnionFind4 implements UF { private int[] parent; /** * rank[i]表示以i爲根的集合所表示的樹的層數 */ private int[] rank; public UnionFind4(int size) { parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } @Override public int getSize() { return parent.length; } /** * 查找元素p所對應的根節點集合編號 * 時間複雜度爲O(h),h爲樹的高度 * @param p * @return */ private int find(int p) { if (p < 0 && p >= parent.length) { throw new IllegalArgumentException("p is out of bound"); } while (p != parent[p]) { p = parent[p]; } return p; } /** * 查看元素p和元素q是否所屬同一個集合 * 時間複雜度爲O(h),h爲樹的高度 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } //根據兩個元素所在樹的rank不一樣判斷合併方向 //將rank低的集合合併到rank高的集合上 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[pRoot] > rank[qRoot]) { parent[qRoot] = pRoot; } else { parent[qRoot] = pRoot; rank[pRoot] += 1; } } }
說明:在基於樹size的優化基礎上,進一步優化,樹的size大,可是樹的深度不必定就大,比較合理的優化方式是基於樹的高度優化,高度小的樹指向高度大的樹。這樣合併操做之後儘可能不會增長樹的高度。ide
/** * 基於數組模擬並查集第四版 * 基於路徑壓縮對並查集合並操做進行優化 */ public class UnionFind5 implements UF { private int[] parent; /** * rank[i]表示以i爲根的集合所表示的樹的rank */ private int[] rank; public UnionFind5(int size) { parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } @Override public int getSize() { return parent.length; } /** * 查找元素p所對應的根節點集合編號 * 時間複雜度爲O(h),h爲樹的高度 * @param p * @return */ private int find(int p) { if (p < 0 && p >= parent.length) { throw new IllegalArgumentException("p is out of bound"); } while (p != parent[p]) { //路徑壓縮,下降樹的深度 parent[p] = parent[parent[p]]; p = parent[p]; } return p; } /** * 查看元素p和元素q是否所屬同一個集合 * 時間複雜度爲O(h),h爲樹的高度 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } //根據兩個元素所在樹的rank不一樣判斷合併方向 //將rank低的集合合併到rank高的集合上 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[pRoot] > rank[qRoot]) { parent[qRoot] = pRoot; } else { parent[qRoot] = pRoot; rank[pRoot] += 1; } } }
說明:在查詢操做時,會對樹路徑進行壓縮,將高度比較高的樹壓縮成高度較低的樹。提高查詢和合並的效率。性能
/** * 基於數組模擬並查集第四版 * 基於路徑壓縮對並查集合並操做進行優化 */ public class UnionFind6 implements UF { private int[] parent; /** * rank[i]表示以i爲根的集合所表示的樹的rank */ private int[] rank; public UnionFind6(int size) { parent = new int[size]; rank = new int[size]; for (int i = 0; i < parent.length; i++) { parent[i] = i; rank[i] = 1; } } @Override public int getSize() { return parent.length; } /** * 查找元素p所對應的根節點集合編號 * 時間複雜度爲O(h),h爲樹的高度 * @param p * @return */ private int find(int p) { if (p < 0 && p >= parent.length) { throw new IllegalArgumentException("p is out of bound"); } if (p != parent[p]) { //路徑壓縮,遞歸找到p元素的根,而後直接將p元素掛到根上,最大限度的壓縮 parent[p] = find(parent[p]); } return parent[p]; } /** * 查看元素p和元素q是否所屬同一個集合 * 時間複雜度爲O(h),h爲樹的高度 * @param p 元素ID * @param q 元素ID * @return */ @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } //根據兩個元素所在樹的rank不一樣判斷合併方向 //將rank低的集合合併到rank高的集合上 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[pRoot] > rank[qRoot]) { parent[qRoot] = pRoot; } else { parent[qRoot] = pRoot; rank[pRoot] += 1; } } }
說明:路徑壓縮的另外一種方式,利用遞歸將樹壓縮到最理想的兩層。最大程度的提高路徑查詢和元素合併操做的性能。測試
import java.util.Random; public class Main { public static void main(String[] args) { //UnionFind1(size=100000,m=10000) : 0.2367079 s testUnionFind1(100000, 10000); //UnionFind1(size=100000,m=100000) : 4.482173601 s testUnionFind1(100000, 100000); //UnionFind2(size=100000,m=10000) : 0.0016591 s testUnionFind2(100000, 10000); //UnionFind2(size=100000,m=100000) : 9.875075801 s testUnionFind2(100000, 100000); //UnionFind3(size=20000000,m=20000000) : 9.055730401 s testUnionFind3(20000000, 20000000); //UnionFind4(size=20000000,m=20000000) : 9.4252049 s testUnionFind4(20000000, 20000000); //UnionFind5(size=20000000,m=20000000) : 8.417531 s testUnionFind5(20000000, 20000000); //UnionFind6(size=20000000,m=20000000) : 7.6787675 s testUnionFind6(20000000, 20000000); } private static void testUnionFind1(int size, int m) { UnionFind1 uf1 = new UnionFind1(size); System.out.println("UnionFind1(size=" + size + ",m=" + m + ") : " + testUF(uf1, m) + " s"); } private static void testUnionFind2(int size, int m) { UnionFind2 uf2 = new UnionFind2(size); System.out.println("UnionFind2(size=" + size + ",m=" + m + ") : " + testUF(uf2, m) + " s"); } private static void testUnionFind3(int size, int m) { UnionFind3 uf3 = new UnionFind3(size); System.out.println("UnionFind3(size=" + size + ",m=" + m + ") : " + testUF(uf3, m) + " s"); } private static void testUnionFind4(int size, int m) { UnionFind4 uf4 = new UnionFind4(size); System.out.println("UnionFind4(size=" + size + ",m=" + m + ") : " + testUF(uf4, m) + " s"); } private static void testUnionFind5(int size, int m) { UnionFind5 uf5 = new UnionFind5(size); System.out.println("UnionFind5(size=" + size + ",m=" + m + ") : " + testUF(uf5, m) + " s"); } private static void testUnionFind6(int size, int m) { UnionFind6 uf6 = new UnionFind6(size); System.out.println("UnionFind6(size=" + size + ",m=" + m + ") : " + testUF(uf6, m) + " s"); } private static double testUF(UF uf, int m) { int size = uf.getSize(); Random random = new Random(); long startTime = System.nanoTime(); //m次合併操做 for (int i = 0; i < m; i++) { int a = random.nextInt(size); int b = random.nextInt(size); uf.unionElements(a, b); } for (int i = 0; i < m; i++) { int a = random.nextInt(size); int b = random.nextInt(size); uf.isConnected(a, b); } long endTime = System.nanoTime(); return (endTime-startTime) / 1000000000.0; } }