并查集深度入门:从玄学抽象到 QuickFind & QuickUnion 源码实战

并查集深度入门:从玄学抽象到 QuickFind & QuickUnion 源码实战

  • [Bilibili 同步视频](#Bilibili 同步视频)
  • 一、先搞懂:并查集的核心------超高抽象度
  • 二、并查集的本命:解决「连通性问题」
    • [2.1 什么是连通性?](#2.1 什么是连通性?)
    • [2.2 连通性的两大核心特性(必考!)](#2.2 连通性的两大核心特性(必考!))
      • [✅ 特性1:双向性](#✅ 特性1:双向性)
      • [✅ 特性2:传递性](#✅ 特性2:传递性)
    • [2.3 连通过程可视化(Plain Text 图解)](#2.3 连通过程可视化(Plain Text 图解))
  • [三、并查集初代实现:QuickFind 快速查找算法](#三、并查集初代实现:QuickFind 快速查找算法)
    • [3.1 核心思想:染色法](#3.1 核心思想:染色法)
    • [3.2 算法优缺点(名字的由来)](#3.2 算法优缺点(名字的由来))
    • [3.3 QuickFind 完整 C++ 源码 & 逐行精讲](#3.3 QuickFind 完整 C++ 源码 & 逐行精讲)
      • [① 头文件与命名空间](#① 头文件与命名空间)
      • [② 并查集类整体框架](#② 并查集类整体框架)
      • [③ 初始化函数精讲](#③ 初始化函数精讲)
      • [④ 查找函数精讲(QuickFind 核心优势)](#④ 查找函数精讲(QuickFind 核心优势))
      • [⑤ 合并函数精讲(QuickFind 核心短板)](#⑤ 合并函数精讲(QuickFind 核心短板))
      • [⑥ 连通判断 & 测试案例](#⑥ 连通判断 & 测试案例)
    • [3.4 关键细节答疑](#3.4 关键细节答疑)
  • [四、并查集二代优化:QuickUnion 快速合并算法](#四、并查集二代优化:QuickUnion 快速合并算法)
    • [4.1 优化痛点](#4.1 优化痛点)
    • [4.2 核心思想:父子节点树形管理](#4.2 核心思想:父子节点树形管理)
    • [4.3 树形结构 Plain Text 图解](#4.3 树形结构 Plain Text 图解)
    • [4.4 算法优缺点反转](#4.4 算法优缺点反转)
    • [4.5 QuickUnion 完整 C++ 源码 & 逐行精讲](#4.5 QuickUnion 完整 C++ 源码 & 逐行精讲)
      • [① 头文件与类结构定义](#① 头文件与类结构定义)
      • [② 树形初始化精讲](#② 树形初始化精讲)
      • [③ 根节点查找函数精讲](#③ 根节点查找函数精讲)
      • [④ 合并函数精讲(QuickUnion 核心优势)](#④ 合并函数精讲(QuickUnion 核心优势))
      • [⑤ 连通判断 & 测试案例](#⑤ 连通判断 & 测试案例)
  • [五、两大算法终极对比 & 进阶预告](#五、两大算法终极对比 & 进阶预告)
  • 六、文末核心总结

很多初学算法的同学,第一次看到**并查集(Union-Find)**时都会一脸懵:这玩意儿到底能干啥?既没有排序花哨,也没有递归精妙,看似平平无奇,却是算法刷题、工程连通场景、图论问题中的「隐形王者」。

今天这篇博客,我们就抛开晦涩的官方定义,用最通俗、带点趣味的大白话 ,从零吃透并查集的核心思想,手把手拆解两大基础实现:QuickFind(快速查找)QuickUnion(快速合并),附带完整可运行的 C++ 源码与复杂度剖析,新手也能一次性学懂!

Bilibili 同步视频

并查集深度入门:从玄学抽象到 QuickFind & QuickUnion 源码实战

一、先搞懂:并查集的核心------超高抽象度

我们先举个所有人都能懂的例子:1+1 到底有什么用?

如果不结合场景,1+1 只是一串无意义的数学符号:

  • 1 代表 1 个苹果,1+1 就是 2 个苹果 🎯

  • 1 代表 1 次旅行,1+1 就是 2 次旅途 ✈️

  • 1 代表 1 个用户,1+1 就是两个用户建立关联 👥

这就是抽象化 的核心:符号本身无意义,场景赋予一切意义

而并查集,就是这样一个抽象度拉满的数据结构。

它不会固定解决某一道题、某一类业务,只要场景需要 维护元素关联关系、查询元素连通状态 ,并查集就能上场。简单来说:谜底就在谜面上,并查集,只管「合并、查询、集合」三件事

二、并查集的本命:解决「连通性问题」

2.1 什么是连通性?

假设平面上有一堆散落的节点,我们不断对节点进行「连接操作」,全程需要维护两个核心能力:

  1. 任意两个节点是否连通?

  2. 连通的节点自动归为一个整体集合

这就是算法中经典的连通性问题,也是并查集唯一的本命场景。

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)

  1. 按秩合并:避免树退化成链表,平衡树的高度

  2. 路径压缩:查找时扁平化树结构,彻底优化查找效率

六、文末核心总结

  1. 并查集是高抽象数据结构,无固定场景,专注解决「连通、合并、查询」问题;

  2. 连通性具备双向性、传递性,是并查集运行的核心逻辑支撑;

  3. QuickFind 用染色法换极速查找,牺牲合并效率;

  4. QuickUnion 用树形结构换极速合并,牺牲查找稳定性;

  5. 所有节点合并,本质都是集合根节点的合并,而非单点合并。

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

相关推荐
比奇堡裤头村1 小时前
统计学习方法——决策树
算法·决策树·学习方法
她说可以呀1 小时前
Spring AI 常用 Advisor
人工智能·python·spring
小玮看世界1 小时前
[Python] str() 和 join() 的区别与实战避坑指南
前端·javascript·python
灵晔君1 小时前
【Linux】进程控制(一)——进程创建、进程终止与进程等待
linux·c语言·算法
卷无止境1 小时前
FastAPI 的 WebSocket 装了些什么
后端·python·fastapi
码云骑士1 小时前
108-vLLM推理引擎-PagedAttention-连续批处理-吞吐提升10倍
python·vllm
DeepVisionary1 小时前
从微软砍掉Copilot的AI播客、Deep Research看AI产品“做减法“:个人版与企业版合并,超级应用呼之欲出
python·自动化
IT·侯老师1 小时前
qq-auto-reply
python
迷迭香yy1 小时前
大宗交易折溢价因子怎么挖掘本地化Python全流程实战
开发语言·人工智能·python