目录
[1. 本质](#1. 本质)
[2. 两个核心操作](#2. 两个核心操作)
[3. 两大优化(必须掌握)](#3. 两大优化(必须掌握))
[二、Java 代码实现(完整版,带优化)](#二、Java 代码实现(完整版,带优化))
[1. 初始化](#1. 初始化)
[2. find () + 路径压缩(关键)](#2. find () + 路径压缩(关键))
[3. union () + 按秩合并(关键)](#3. union () + 按秩合并(关键))
[4. 常用工具方法](#4. 常用工具方法)
并查集(Union-Find / Disjoint Set Union,DSU )是一种处理动态连通性问题的数据结构,核心解决两个问题:
- 查询:两个元素是否在同一个集合中
- 合并:将两个不同的集合合并为一个
它的效率极高,均摊时间复杂度接近 O (1),是图论、最小生成树(Kruskal)、朋友圈问题、岛屿问题的核心算法。
一、核心概念
1. 本质
用一个数组parent[] 表示元素的父节点:
parent[i] = j:元素i的父节点是j- 根节点 :
parent[i] = i(自己是自己的父节点,代表整个集合)
2. 两个核心操作
- find(x) :查找元素
x所在集合的根节点(用于判断连通性) - union(x, y) :合并
x和y所在的两个集合
3. 两大优化(必须掌握)
不优化的并查集会退化成链表,效率极低,必须加这两个优化:
- 路径压缩:查询时直接让节点指向根节点,抹平树的高度
- 按秩合并 :合并时把小树挂到大树下,避免树越来越高
二、Java 代码实现(完整版,带优化)
这是工业级标准实现,支持初始化、查询、合并、统计连通分量,直接复制可用。
public class UnionFind {
// 父节点数组:parent[i] 表示 i 的父节点
private int[] parent;
// 秩数组:记录树的高度/大小,用于按秩合并
private int[] rank;
// 连通分量的个数(独立集合的数量)
private int count;
// 构造方法:初始化并查集,元素范围 0 ~ n-1
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
count = n; // 初始每个元素自己是一个集合
for (int i = 0; i < n; i++) {
parent[i] = i; // 初始父节点是自己
rank[i] = 1; // 初始树高度为1
}
}
// 【核心1】查找根节点 + 路径压缩
public int find(int x) {
// 递归写法:简洁,推荐
if (parent[x] != x) {
// 路径压缩:直接让x指向根节点
parent[x] = find(parent[x]);
}
return parent[x];
// 迭代写法(避免栈溢出,大数据量推荐)
/*
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // 路径压缩
x = parent[x];
}
return x;
*/
}
// 【核心2】合并两个集合 + 按秩合并
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
// already in the same set
if (rootX == rootY) {
return;
}
// 按秩合并:小树挂到大树下
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
// 高度相同,合并后高度+1
parent[rootY] = rootX;
rank[rootX]++;
}
// 合并成功,连通分量-1
count--;
}
// 判断两个元素是否连通
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
// 获取当前连通分量的数量
public int getCount() {
return count;
}
}
三、代码核心讲解
1. 初始化
- 每个元素的父节点是自己,自己是独立集合
count初始等于元素总数
2. find () + 路径压缩(关键)
parent[x] = find(parent[x]);
作用:让查询路径上的所有节点直接指向根节点,下次查询直接 O (1) 找到根。
3. union () + 按秩合并(关键)
- 先找两个节点的根
- 根不同才合并
- 小树挂大树,保证树始终是 "矮胖" 的,效率最高
4. 常用工具方法
isConnected(x,y):判断连通getCount():获取独立集合数量
四、测试代码(验证功能)
public class Main {
public static void main(String[] args) {
// 初始化 5 个元素:0,1,2,3,4
UnionFind uf = new UnionFind(5);
// 合并 0-1,2-3,0-2
uf.union(0, 1);
uf.union(2, 3);
uf.union(0, 2);
// 查询
System.out.println(uf.isConnected(0, 3)); // true
System.out.println(uf.isConnected(0, 4)); // false
System.out.println(uf.getCount()); // 2 个连通分量:{0,1,2,3}, {4}
}
}
五、并查集适用场景(高频面试题)
只要题目出现连通、合并、分组、朋友圈、岛屿、家族等关键词,直接用并查集:
- 朋友圈问题:多少个朋友圈
- 岛屿数量:二维网格并查集
- 最小生成树 Kruskal 算法
- 判断图是否有环
- 等式方程的可满足性
- 冗余连接
六、二维网格并查集(扩展,面试常考)
如果是二维坐标 (x,y),只需要做一个坐标映射:
// 二维坐标转一维:x 行 y 列,总列数 cols
int index = x * cols + y;
就能直接用上面的并查集类。
总结
- 并查集 = 查找 (find) + 合并 (union)
- 两大优化:路径压缩 + 按秩合并(必须写)
- 时间复杂度:几乎 O (1)
- 适用场景:动态连通性问题