并查集是一种比较独特的数据结构,它是由孩子节点指向父亲节点的树形结构。并查集能高效地解决链接问题(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; } }