数据结构学习笔记 - 12 并查集
介绍
已知两个元素,如何快速判断两个元素是否属于同一个集合?已知两个集合,如何将两个集合合并为同一个集合?并查集就是解决这两个问题的数据结构。
并查集可以非常快判断网络中节点间的连接状态。
并查集是特殊的树结构,它是又孩子指向父亲。
注意并查集只关注两个点的连接问题,而不关注两个点的路径问题(路径问题显然比只关注连接要更复杂)。
并查集也不考虑添加和删除元素,只考虑当下的元素进行并或者查的操作。对于一组数据,并查集主要支持两个动作:
- unionElements(p, q),即将 p 和 q 两个元素所属的集合合并;
- isConnected(p, q),判断 p 和 q 两个元素是否属于同一个集合。
接口
并查集接口
public interface UF {
int getSize();
boolean isConnected(int p, int q);
void unionElements(int p, int q);
}
Quick Find
这种实现方式只是在查询上比较快速,在合并操作上并不高效,并不是并查集的最终实现。
/**
* 我们可以将数据按照如下方式存储,每个元素对应一个 id,id 相同即代表元素属于同一个集合。
*
* 0 1 2 3 4 5 6 7 8 9
* id 0 1 0 1 0 1 0 1 0 1
*
* 表示 0 2 4 6 8 在一个集合中,1 3 5 7 9 在另一个集合中。
*
* 此时,查找一个元素属于哪个集合就会非常快,只需要 O(1) 的复杂度。
*
* 操作 复杂度
* isConnected(p, q) O(1)
* unionElements(p, q) O(n)
*/
public class UnionFind implements UF {
private int[] id; // 这个并查集本质就是一个数组。
public UnionFind(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 所对应的集合编号。
* 复杂度:O(1)
*/
private int find(int p) {
if (p < 0 || p >= id.length) {
throw new IllegalArgumentException("p is out of bound.");
}
return id[p];
}
/**
* 查找元素 p 和元素 q 是否属于同一个集合。
* 复杂度:O(1)
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
/**
* 合并元素 p 和元素 q 所属的集合。
* 复杂度:O(n)
*/
@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
带路径压缩优化的 Quick Union 并查集实现。
/**
* Quick Union 是并查集的一种更高效的实现。
* 它将每一个元素都看作是一个节点。
*
* 0 1 2 3 4 5 6 7 8 9
* parent 1 1 1 8 3 0 5 1 8 8
*
* 上面的内容可以用下面两个树来表示,分别表示两个集合;下面两棵树都是由孩子指向父亲的,根节点指向自己。
*
* 1 8
* / | \ | \
* 0 2 7 3 9
* | |
* 5 4
* |
* 6
*
* 操作 复杂度
* isConnected(p, q) O(h)
* unionElements(p, q) O(h)
*
* 如果加上路径压缩优化后的并查集,上述复杂度可以表示为 O(log*n),注意 * 可以称为「星」,它不是乘号,这是一个比 O(logn) 更快的更接近的 O(1) 级别的复杂度。
*/
public class UnionFind implements UF {
// parent[i] 表示第 i 个元素指向的父节点
private int[] parent;
// rank[i] 表示以 i 为根的集合所表示的层数
private int[] rank;
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
// 复杂度为 O(h),其中 h 为树的高度。
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;
}
// 复杂度为 O(h),其中 h 为树的高度。
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(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 (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}