很多初学算法的同学,第一次看到**并查集(Union-Find)**时都会一脸懵:这玩意儿到底能干啥?既没有排序花哨,也没有递归精妙,看似平平无奇,却是算法刷题、工程连通场景、图论问题中的「隐形王者」。
今天这篇博客,我们就抛开晦涩的官方定义,用最通俗、带点趣味的大白话 ,从零吃透并查集的核心思想,手把手拆解两大基础实现:QuickFind(快速查找)和QuickUnion(快速合并),附带完整可运行的 C++ 源码与复杂度剖析,新手也能一次性学懂!
Bilibili 同步视频
一、先搞懂:并查集的核心------超高抽象度
我们先举个所有人都能懂的例子:1+1 到底有什么用?
如果不结合场景,1+1 只是一串无意义的数学符号:
-
1 代表 1 个苹果,1+1 就是 2 个苹果 🎯
-
1 代表 1 次旅行,1+1 就是 2 次旅途 ✈️
-
1 代表 1 个用户,1+1 就是两个用户建立关联 👥
这就是抽象化 的核心:符号本身无意义,场景赋予一切意义。
而并查集,就是这样一个抽象度拉满的数据结构。
它不会固定解决某一道题、某一类业务,只要场景需要 维护元素关联关系、查询元素连通状态 ,并查集就能上场。简单来说:谜底就在谜面上,并查集,只管「合并、查询、集合」三件事。
二、并查集的本命:解决「连通性问题」
2.1 什么是连通性?
假设平面上有一堆散落的节点,我们不断对节点进行「连接操作」,全程需要维护两个核心能力:
-
任意两个节点是否连通?
-
连通的节点自动归为一个整体集合
这就是算法中经典的连通性问题,也是并查集唯一的本命场景。
2.2 连通性的两大核心特性(必考!)
✅ 特性1:双向性
A 和 B 连通,等价于 B 和 A 连通。就像朋友关系:我是你的朋友,你必然是我的朋友(算法理想化模型,排除现实勾心斗角😏)。
✅ 特性2:传递性
A 连通 B、B 连通 C ➡️ 可直接推导:A 连通 C。
最经典的场景:朋友圈圈层
我和甲是朋友,甲和乙是朋友,在并查集的逻辑里,我和乙自动属于同一个朋友圈集合。
温馨提示 :现实中存在「A连B、B连C,但A和C不熟」的情况,但算法中的连通是纯逻辑理想化传递关系,无需考虑特殊情况!
2.3 连通过程可视化(Plain Text 图解)
我们以 10 个节点(1~10)为例,模拟一组连接操作:
操作顺序:4-3 相连 → 3-8 相连 → 6-5 相连 → 9-4 相连 → 1-2 相连 → 2-7 相连 → 1-6 相连
Plain
初始状态:[1][2][3][4][5][6][7][8][9][10] (10个独立集合)
4-3连通:[1][2][3,4][5][6][7][8][9][10]
3-8连通:[1][2][3,4,8][5][6][7][9][10]
6-5连通:[1][2][3,4,8][5,6][7][9][10]
9-4连通:[1][2][3,4,8,9][5,6][7][10]
1-2连通:[1,2][3,4,8,9][5,6][7][10]
2-7连通:[1,2,7][3,4,8,9][5,6][10]
1-6连通:[1,2,7,5,6][3,4,8,9][10]
最终集合:两大连通集群 + 独立节点10
集群1:1、2、5、6、7
集群2:3、4、8、9
独立节点:10
💡 核心结论:所有连接操作,本质都不是连接两个点,而是合并两个完整集合!
三、并查集初代实现:QuickFind 快速查找算法
3.1 核心思想:染色法
QuickFind 是并查集最朴素的实现方式,我们可以用「染色」的逻辑秒懂:
给每一个集合分配专属颜色,同集合节点颜色完全一致,不同集合颜色不同。
-
查询连通:对比两个节点颜色,相同即连通,不同即独立 🚀
-
合并集合:将其中一个集合的所有节点,全部染成另一个集合的颜色 🎨
3.2 算法优缺点(名字的由来)
-
优点 :查找操作极致快,时间复杂度 O(1),因此得名 QuickFind
-
缺点 :合并操作巨慢,需要遍历所有节点,时间复杂度 O(n)
小趣味答疑:为什么不叫「SlowUnion」?
算法命名和面试自我介绍一个道理:只秀优点,不曝短板,谁会给自己的算法起一个听起来很弱的名字呢😂
3.3 QuickFind 完整 C++ 源码 & 逐行精讲
下面我们对 QuickFind 算法代码进行模块化拆分讲解,整体分为:头文件、类定义、初始化、查找、合并、连通判断、测试入口,每一段都对应专属功能。
① 头文件与命名空间
引入输入输出与可变数组容器,适配动态节点数量,是算法通用基础配置。
cpp
#include <iostream>
#include <vector>
using namespace std;
② 并查集类整体框架
QuickFind 核心依靠「颜色数组」区分集合,私有成员存储节点数量与颜色标记,公有成员提供对外操作接口。
cpp
// QuickFind 并查集
class UnionFind {
private:
vector<int> color; // 核心:存储每个节点的集合颜色
int n; // 节点总数量
public:
// 构造函数:初始化并查集
UnionFind(int size);
// 查找操作:获取节点所属集合
int find(int x);
// 合并操作:合并两个节点所在集合
void unite(int x, int y);
// 判断两点是否连通
bool isConnected(int x, int y);
};
③ 初始化函数精讲
核心规则:初始状态每个节点独立成集合,自身编号即为专属颜色。适配刷题常用的 1~n 节点编号,数组开辟 n+1 空间,规避下标 0 无效问题。
cpp
UnionFind(int size) {
n = size;
color.resize(n + 1); // 节点范围 1~n,扩容 n+1
for (int i = 1; i <= n; i++) {
color[i] = i; // 每个节点初始颜色 = 自身编号
}
}
④ 查找函数精讲(QuickFind 核心优势)
直接读取节点颜色,无需循环递归,时间复杂度 O(1),这也是 QuickFind「快速查找」名字的由来。返回值即为节点所属集合的唯一标识。
cpp
int find(int x) {
return color[x];
}
⑤ 合并函数精讲(QuickFind 核心短板)
先判断两节点是否同集合,避免无效操作;若不同,则遍历所有节点,将其中一个集合的全部节点统一染色,完成集合合并。需要全量遍历,因此合并效率极低,复杂度 O(n)。
cpp
void unite(int x, int y) {
int cx = find(x);
int cy = find(y);
// 优化:同一集合,无需合并
if (cx == cy) return;
// 遍历所有节点,将 y 集合全部染为 x 集合颜色
for (int i = 1; i <= n; i++) {
if (color[i] == cy) {
color[i] = cx;
}
}
}
⑥ 连通判断 & 测试案例
连通判断依托查找功能,两节点颜色一致即属于同一连通集合。主函数模拟前文节点连通过程,直观验证算法效果。
cpp
bool isConnected(int x, int y) {
return find(x) == find(y);
}
// 测试Demo
int main() {
UnionFind uf(10);
uf.unite(4, 3);
uf.unite(3, 8);
uf.unite(6, 5);
cout << "4和8是否连通:" << (uf.isConnected(4,8) ? "是" : "否") << endl;
cout << "4和6是否连通:" << (uf.isConnected(4,6) ? "是" : "否") << endl;
return 0;
}
3.4 关键细节答疑
Q1:为什么数组要开 n+1?
A:完全由场景决定!节点编号 1~n 开 n+1,节点编号 0~n-1 开 n 即可,代码灵活性极高。
Q2:可以把 x 染成 y 的颜色吗?
A:完全可以!就像两个国家合并,只是改个名字,集合整体结构、连通关系不会发生任何变化。
四、并查集二代优化:QuickUnion 快速合并算法
4.1 优化痛点
QuickFind 的致命短板:合并必须遍历全数组,数据量大时效率崩盘。
为了解决合并慢的问题,QuickUnion 横空出世,核心思路:抛弃染色,改用树形结构。
4.2 核心思想:父子节点树形管理
我们不再给节点染色,而是用一个 father[] 数组记录每个节点的父节点:
-
初始化:每个节点的父节点是自己
father[i] = i -
自己是自己的父节点 = 树的根节点(集合老大/CEO)
-
同一棵树的所有节点,属于同一个连通集合
4.3 树形结构 Plain Text 图解
依旧以 4、3、8、9 连通为例:
Plain
初始:4 3 8 9 各自为根
4-3连通:3 的父节点 = 4
树结构:4 ← 3
3-8连通:8 的父节点 = 3
树结构:4 ← 3 ← 8
9-4连通:9 的父节点 = 4
最终树:
4
/
3 9
/
8
所有节点归属同一棵树,即属于同一个连通集合,无需全员染色!
4.4 算法优缺点反转
-
优点:合并操作仅需修改父节点指向,仅 1 行代码,时间复杂度近似 O(1),极速合并 ✅
-
缺点:查找根节点需要逐层向上遍历,最坏情况树退化成链表,复杂度退化至 O(n) ❌
4.5 QuickUnion 完整 C++ 源码 & 逐行精讲
QuickUnion 彻底抛弃染色思路,采用树形父子节点结构,我们同样模块化拆分,逐段解析核心逻辑,对比 QuickFind 的优化与短板。
① 头文件与类结构定义
将颜色数组替换为父节点数组 father,不再记录集合颜色,只记录每个节点的上级节点,搭建树形结构基础。
cpp
#include <iostream>
#include <vector>
using namespace std;
// QuickUnion 并查集
class UnionFind {
private:
vector<int> father; // 核心:存储每个节点的父节点编号
int n; // 节点总数量
public:
// 初始化:构建初始树形结构
UnionFind(int size);
// 查找:递归寻找节点的根节点
int find(int x);
// 合并:合并两个节点所在的树根
void unite(int x, int y);
// 判断连通性
bool isConnected(int x, int y);
};
② 树形初始化精讲
初始化规则:每个节点的父节点默认为自己。自己指向自己的节点,即为一棵树的根节点(集合最高上级),所有节点初始都是独立树根。
cpp
UnionFind(int size) {
n = size;
father.resize(n + 1);
for (int i = 1; i <= n; i++) {
father[i] = i; // 初始:自己是自己的父节点(独立树根)
}
}
③ 根节点查找函数精讲
递归向上追溯父节点,直到找到「自指向」的根节点。同一棵树的所有节点,最终都会找到同一个根节点,以此判定同集合。最坏情况树退化成链表,复杂度 O(n)。
cpp
int find(int x) {
// 递归终止:找到根节点
if (father[x] == x) {
return x;
}
// 向上递归查找父节点的根
return find(father[x]);
}
④ 合并函数精讲(QuickUnion 核心优势)
合并的本质:合并两棵树的根节点,而非合并两个单点。仅需修改一行父节点指向,即可完成集合合并,操作极快,近似 O(1),也是 QuickUnion「快速合并」的核心原因。
cpp
void unite(int x, int y) {
// 分别找到两个节点所在树的根
int rootX = find(x);
int rootY = find(y);
// 同根节点,属于同一集合,无需合并
if (rootX == rootY) return;
// 将 x 所在树根,挂载到 y 所在树根下,完成树合并
father[rootX] = rootY;
}
⑤ 连通判断 & 测试案例
连通判定逻辑:两个节点根节点相同,即为连通。测试案例对应前文 4、3、8、9 节点连通场景,验证树形合并效果。
cpp
bool isConnected(int x, int y) {
return find(x) == find(y);
}
// 测试Demo
int main() {
UnionFind uf(10);
uf.unite(4, 3);
uf.unite(3, 8);
uf.unite(9, 4);
cout << "3和9是否连通:" << (uf.isConnected(3,9) ? "是" : "否") << endl;
cout << "8和9是否连通:" << (uf.isConnected(8,9) ? "是" : "否") << endl;
return 0;
}
五、两大算法终极对比 & 进阶预告
| 算法 | 核心原理 | 查找复杂度 | 合并复杂度 | 核心特点 |
|---|---|---|---|---|
| QuickFind | 染色标记集合 | O(1) | O(n) | 查找极快,合并拉胯 |
| QuickUnion | 树形父子节点 | 最坏O(n) | O(1) | 合并极快,查找不稳定 |
后续进阶优化方向
目前的两种算法各有短板,后续我们可以通过两大优化,将并查集复杂度压缩到近乎 O(1):
-
按秩合并:避免树退化成链表,平衡树的高度
-
路径压缩:查找时扁平化树结构,彻底优化查找效率
六、文末核心总结
-
并查集是高抽象数据结构,无固定场景,专注解决「连通、合并、查询」问题;
-
连通性具备双向性、传递性,是并查集运行的核心逻辑支撑;
-
QuickFind 用染色法换极速查找,牺牲合并效率;
-
QuickUnion 用树形结构换极速合并,牺牲查找稳定性;
-
所有节点合并,本质都是集合根节点的合并,而非单点合并。

后续会持续更新路径压缩、按秩合并的最优并查集源码,以及刷题实战案例,感兴趣可以持续关注!✨