前言:
大家好,很多公司都考过并查集,那其实我就很不理解,为啥对于并查集的考察这么频繁呢?
后来,才发现,原来并查集有这么多实际的用途。
什么是一个并查集
想象一下,你是一个幼儿园老师,班上有10个小朋友。每天自由活动时,小朋友们会自发组成小团体玩耍。并查集就是帮你快速回答以下问题的工具:
- 小明和小红是不是在同一个团体里玩?(查找)
- 如果小明和小红的团体要合并,怎么操作?(合并)
代码的实现
public class DisjointSetUnion {
private int[] parent; // 记录每个节点的父节点
private int[] rank; // 记录树的深度(用于优化)
// 构造函数:初始化并查集
public DisjointSetUnion(int size) {
parent = new int[size];
rank = new int[size];
// 初始时,每个元素都是自己的父节点(自己是自己的老大)
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1; // 初始深度为1
}
}
// 查找:找到元素x的根节点(终极老大)
public int find(int x) {
if (parent[x] != x) {
// 路径压缩:让x直接指向根节点,缩短下次查找路径
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并:把x和y所在的集合合并
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
// 如果已经在同一个集合,不需要合并
if (rootX == rootY) {
return;
}
// 按秩合并:小树挂在大树下,保持平衡
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++; // 两棵树深度相同,合并后深度+1
}
}
// 检查x和y是否在同一个集合
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
}
代码深度讲解
1. 初始化阶段 - "各自为王"
public DisjointSetUnion(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; 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) return; // 已经是同一个帮派
// 按秩合并
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY; // 小帮派并入大帮派
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++; // 两个帮派规模相同,合并后高度+1
}
}
这就像两个小朋友的团体要合并:
- 先找出各自的老大
- 如果老大相同,说明本来就是一伙的
- 如果不同,就让规模小的团体并入规模大的团体(按秩合并)
- 这样做可以保持树的平衡,防止退化成链表
查询连接 - "是不是一伙的"
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
这个最简单:"小明和小红的老大是同一个人吗?"如果是,他们就是一伙的!
时间复杂度分析
- 不使用优化:最坏O(n)(退化成链表)
- 使用路径压缩和按秩合并:接近O(1)(阿克曼函数的反函数,增长极其缓慢)
实际应用场景
- 朋友圈关系(真的像微信朋友圈!)
- 游戏中的连通区域检测
- Kruskal最小生成树算法
- 图像处理中的像素连通区域
举个栗子🌰
public static void main(String[] args) {
DisjointSetUnion dsu = new DisjointSetUnion(10); // 10个小朋友
dsu.union(0, 1); // 0和1成为朋友
dsu.union(1, 2); // 1和2成为朋友 → 0,1,2现在是朋友
dsu.union(3, 4); // 3和4成为朋友
System.out.println("1和2是朋友吗? " + dsu.isConnected(1, 2)); // true
System.out.println("0和3是朋友吗? " + dsu.isConnected(0, 3)); // false
dsu.union(2, 3); // 合并两个朋友圈
System.out.println("现在0和3是朋友吗? " + dsu.isConnected(0, 3)); // true
}
总结
并查集就像是一个高效的"社交关系管理员",它能:
- 快速判断两个人是否有共同朋友(连通性)
- 高效合并两个社交圈(合并操作)
- 通过路径压缩和按秩合并保持高效率
记住这个数据结构的秘诀:找老大要记得抄近路,合并帮派要看规模!
每日鸡汤:
fake it, until you make it.