并查集(Union-Find,又称不相交集合,Disjoint Set)是一种用于处理 动态连通性问题 的数据结构。它支持两种操作:
- 合并(Union):将两个不相交的集合合并为一个集合。
- 查找(Find):查询某个元素所属的集合。
并查集的核心在于用树结构来表示集合,通过两个关键优化技术:路径压缩 和 按秩合并,并查集可以在接近常数时间内(接近 O(1))完成这两种操作。
1. 并查集的定义
并查集维护多个不相交的集合,主要提供以下两个操作:
- Find(x):找到元素 x 所在集合的代表元素(通常称为根节点)。
- Union(x, y):将元素 x 和 y 所在的两个集合合并为一个集合。
通过这两个操作,能够高效解决动态连通性问题,如判断图中两点是否连通、动态合并多个集合等。
2. 并查集的结构
并查集使用 树形结构 表示集合:
- 每个集合通过一棵树表示,树的每个节点是集合中的元素,根节点是该集合的代表元素。
- 每个节点都有一个指向其父节点的指针,根节点的父节点指向它自身。
并查集的初始状态是每个元素各自为一个独立的集合,这意味着每个元素都是其自身集合的代表元素。
3. 并查集的基本操作
3.1 查找操作(Find)
查找操作用于找到元素所在集合的根节点。根节点是集合中的代表元素,可以通过递归找到。
基本查找操作的时间复杂度为 O(n),因为在最坏情况下,集合可能呈现链式结构,即每个节点只有一个子节点。然而,通过 路径压缩 优化,可以极大地降低查找操作的时间复杂度。
路径压缩 是一种优化技术,在执行查找时,使得树的深度尽可能地降低。具体做法是,在查找某个节点的根节点时,将该节点及其经过的所有节点直接指向根节点,形成扁平化的树结构,从而加速后续的查找操作。
// 查找操作,带路径压缩优化
public int find(int x) {
if (parent[x] != x) {
// 递归找到根节点,并将当前节点指向根节点
parent[x] = find(parent[x]);
}
return parent[x];
}
3.2 合并操作(Union)
合并操作用于将两个不相交的集合合并为一个集合。基本合并操作是将其中一个集合的根节点指向另一个集合的根节点。然而,为了避免树的深度过大,合并时通常使用 按秩合并 或 按大小合并 技术进行优化。
按秩合并 的思路是将秩(或高度)较小的树合并到秩较大的树上,从而保持树的高度尽可能小。每个集合都有一个秩(或称为“树的高度”),当两棵树合并时,将秩较小的树根节点指向秩较大的树根节点。
// 按秩合并
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
4. 并查集的优化
并查集的两个重要优化技术是 路径压缩 和 按秩合并。这两个技术结合在一起,可以使得并查集的时间复杂度接近常数,即 O(α(n)),其中 α(n) 是反阿克曼函数,非常缓慢增长,几乎可以认为是常数。
4.1 路径压缩
路径压缩的作用是在查找操作过程中,将经过的所有节点直接指向根节点,从而将树压扁,使得树的高度尽可能小。
4.2 按秩合并
按秩合并的作用是保证合并操作时,树的高度尽可能小。通过将秩较小的树合并到秩较大的树上,可以防止树的高度增加太快。
5. 并查集的实现
以下是并查集的 Java 实现,结合了路径压缩和按秩合并优化:
class UnionFind {
private int[] parent; // 存储每个节点的父节点
private int[] rank; // 记录每个集合的秩(高度)
// 初始化n个元素,每个元素各自独立成一个集合
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // 每个节点的父节点初始化为自己
rank[i] = 1; // 初始每棵树的高度为1
}
}
// 查找操作,带路径压缩
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并操作,带按秩合并
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX; // 秩高的作为根
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++; // 如果相等,则根的秩加1
}
}
}
// 判断两个元素是否在同一个集合
public boolean connected(int x, int y) {
return find(x) == find(y);
}
}
public class Main {
public static void main(String[] args) {
UnionFind uf = new UnionFind(10);
uf.union(1, 2);
uf.union(2, 3);
uf.union(4, 5);
uf.union(6, 7);
System.out.println("1和3是否连通: " + uf.connected(1, 3)); // 输出: true
System.out.println("4和7是否连通: " + uf.connected(4, 7)); // 输出: false
uf.union(5, 6);
System.out.println("4和7是否连通: " + uf.connected(4, 7)); // 输出: true
}
}
6. 并查集的应用
并查集广泛应用于解决动态连通性问题,以下是常见的应用场景:
- 连通性问题:判断图中两个节点是否连通。
- 最小生成树:在 Kruskal 算法 中使用并查集来检测边的加入是否形成环,从而构造最小生成树。
- 网络连接问题:动态判断某个网络系统中两台机器是否可以通过直接或间接的连接通信。
- 动态连通性检测:用于动态维护系统中某些对象的连通性关系,适合处理合并和查询的操作。
7. 并查集的时间复杂度
由于并查集采用了路径压缩和按秩合并的优化,单次查询和合并的时间复杂度均为 O(α(n)),其中 α(n) 是反阿克曼函数,其增长非常缓慢,几乎可以认为是常数。因此,整体的时间复杂度接近 O(1),非常高效。
8. 总结
并查集是一种高效解决动态连通性问题的数据结构,适用于判断多个元素间的连通性和合并操作。通过路径压缩和按秩合并等优化技术,并查集可以在接近常数时间内完成查找和合并操作。它在图论、网络连通性、最小生成树等领域有广泛应用,尤其适合处理大量动态操作的场景。