一、什么是并查集
一种很不一样的树形结构
连接问题——Connectivity Problem
- 网络中节点间的链接状态
- 网络是个抽象的概念:用户之间形成的网络
- 数学中的集合类实现
连接问题和路径问题
连接问题比路径问题要回答的问题少
public interface UF { int getSize(); boolean isConnected(int p, int q); void unionElements(int p, int q); }
二、Quick Find
package UnionFind; /** * 数组并查集 */ 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所对应的集合编号 private int find(int p){ if (p<0 && p>=id.length){ throw new IllegalArgumentException("p is out of bound."); } return id[p]; } // 查看元素p和元素q是否所属一个集合 @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElements(int p, int q) { int pID = find(p); int qID = find(q); if (pID==qID){ return; } for (int i=0;i<id.length;i++){ if (id[i] == pID){ id[i] = qID; } } } }
三、Quick Union
将每一个元素,看做是一个节点
package UnionFind; /** * 第一版树的并查集 */ public class UnionFind2 implements UF { private int[] parent; public UnionFind2(int size) { parent = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; } } @Override public int getSize() { return parent.length; } // 查找过程,查找元素p所对应的集合编号 // O(h)复杂度,h为树的高度 private int find(int p) { while (p != parent[p]) { p = parent[p]; } return p; } // 查看元素p和元素q是否所属一个集合 // O(h)复杂度,h为树的高度 @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } // 合并元素p和元素q所属的集合 // O(h)复杂度,h为树的高度 @Override public void unionElements(int p, int q) { int pRoot = find(p); int qRoot = find(q); if (pRoot == qRoot) { return; } parent[pRoot] = qRoot; } }
我们这里可以看出来我们用数组和用树(其实应该说是森林)之间的区别
我们来看最终的并查集两种不同的显示结果
相信可以很快的看出结果。
测试一下
可以看到树居然会更慢了
这种的优化思路是非常简单的,让我们来看一下具体的代码实现:
package UnionFind; /** * 优化第一版的树 */ public class UnionFind3 implements UF { private int[] parent; private int[] sz; //sz[i] 表示为以i为根的集合中元素个数 public UnionFind3(int size) { parent = new int[size]; sz = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; sz[i] = 1; } } @Override public int getSize() { return parent.length; } // 查找过程,查找元素p所对应的集合编号 // O(h)复杂度,h为树的高度 private int find(int p) { while (p != parent[p]) { p = parent[p]; } return p; } // 查看元素p和元素q是否所属一个集合 // O(h)复杂度,h为树的高度 @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } // 合并元素p和元素q所属的集合 // O(h)复杂度,h为树的高度 @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 { // sz[qRoot] <= sz[pRoot] parent[qRoot] = pRoot; sz[pRoot] += sz[qRoot]; } } }
优化过后
四、路径压缩
看两张图,清晰可知
代码实现
// 查找过程,查找元素p所对应的集合编号 // O(h)复杂度,h为树的高度 private int find(int p) { while (p != parent[p]) { parent[p] = parent[parent[p]]; p = parent[p]; } return p; }
仅仅一行代码
结果如下
五、对并查集继续优化
我们可不可以直接优化成下面这种直接指向根节点的情况呢?
代码实现
// 查找过程,查找元素p所对应的集合编号 // O(h)复杂度,h为树的高度 private int find(int p) { if (p != parent[p]) { parent[p] = find(parent[p]); } return p; }
测试一下
六、并查集的时间复杂度
本文作者为DBC,转载请注明。