跳到主要内容算法与数据结构:并查集(Union-Find) | 极客日志C++算法
算法与数据结构:并查集(Union-Find)
介绍并查集(Union-Find)数据结构,涵盖核心概念、基础实现及路径压缩、按秩合并两大优化。内容包含 C++ 代码示例,讲解初始化、查找、合并操作,以及带权并查集和扩展域并查集的扩展应用。通过无向图环检测、岛屿数量统计、Kruskal 最小生成树等经典场景展示其用途,并分析时间与空间复杂度。
CodeArtist6 浏览 并查集(Union-Find)是一种高效管理动态集合的数据结构,核心解决两个问题:「查询两个元素是否属于同一集合」和「合并两个集合」。它广泛应用于图论(如连通分量检测、最小生成树 Kruskal 算法)、社交网络(好友关系连通性)、集合分类等场景,其优化后的时间复杂度接近常数级,是算法面试中的高频考点。
一、并查集的核心概念
1. 基本定义
- 元素与集合:每个元素初始时属于一个独立的集合(仅包含自身),集合用「根节点」标识(根节点的父节点是自身)。
- 核心操作:
find(x):查询元素x所在集合的根节点(判断两个元素是否同集,只需比较根节点是否相同)。
union(x, y):将元素x和y所在的两个集合合并为一个集合。
init(n):初始化n个独立集合(父节点数组、秩/大小数组初始化)。
- 设计思想:用「树结构」表示集合(每个集合是一棵多叉树),根节点是集合的唯一标识,通过父节点指针串联所有元素。
二、朴素并查集实现(无优化)
1. 数据结构设计
用两个数组存储核心信息(C++中常用vector实现动态大小):
parent[]:parent[i]表示元素i的父节点,初始时parent[i] = i(自环为根)。
2. 核心操作实现
(1)初始化
#include <vector>
using namespace std;
class UnionFind {
private:
vector<int> parent;
public:
UnionFind(int n) {
parent.resize(n);
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
};
(2)find 操作(递归版)
递归查找根节点,直到找到parent[x] == x的节点:
int find(int x) {
if (parent[x] != x) {
return find(parent[x]);
}
return x;
}
(3)union 操作(朴素版)
找到两个元素的根节点,将其中一个根节点的父节点指向另一个根节点:
void unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
parent[rootY] = rootX;
}
}
3. 朴素版本的问题
- find 操作效率低:树可能退化为链表(如依次合并 1→2→3→4→…),此时
find(x)的时间复杂度为O(n)。
- union 操作无策略:随机合并导致树的高度激增,进一步恶化查询效率。
例如,若合并顺序为unite(0,1)、unite(1,2)、unite(2,3)...,树会变成一条长链,查询末尾元素的根节点需遍历所有节点。
三、并查集的两大优化
为将时间复杂度优化到近似O(α(n))(α是阿克曼函数的反函数,n为元素个数时α(n)≤5,可视为常数),需实现以下两个优化:
1. 优化一:路径压缩(Path Compression)
原理
在find(x)查询时,将路径上所有节点的父节点直接指向根节点,扁平化树结构,减少后续查询的路径长度。
实现(循环版,避免递归栈溢出)
int find(int x) {
int root = x;
while (parent[root] != root) {
root = parent[root];
}
while (parent[x] != root) {
int next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
效果
一次find(x)后,后续查询该路径上的任何节点,都能直接找到根节点,查询效率大幅提升。
2. 优化二:按秩合并(Union by Rank)/ 按大小合并(Union by Size)
原理
合并两个集合时,让'矮树'挂到'高树'的根上(按秩合并)或'小树'挂到'大树'的根上(按大小合并),避免树的高度无意义增长。
- 秩(Rank):表示树的高度上限(初始时每个节点的秩为 1)。
- 大小(Size):表示集合的元素个数(初始时每个集合的大小为 1)。
实现(按秩合并)
class UnionFind {
private:
vector<int> parent;
vector<int> rank;
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 1);
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
void unite(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 {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) {
rank[rootX]++;
}
}
}
};
实现(按大小合并)
class UnionFind {
private:
vector<int> parent;
vector<int> size;
public:
UnionFind(int n) {
parent.resize(n);
size.resize(n, 1);
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
int find(int x) {
int root = x;
while (parent[root] != root) root = parent[root];
while (parent[x] != root) {
int next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
void unite(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return;
if (size[rootX] < size[rootY]) {
parent[rootX] = rootY;
size[rootY] += size[rootX];
} else {
parent[rootY] = rootX;
size[rootX] += size[rootY];
}
}
};
选择建议
- 按秩合并和按大小合并效果类似,均能保证树的高度不超过
logn。
- 按大小合并更直观(可直接获取集合元素个数),按秩合并在某些场景(如带权并查集)更灵活。
四、并查集的核心功能扩展
除了基础的find和unite,实际应用中常需以下扩展功能:
1. 查询集合元素个数
基于按大小合并的size[]数组,查询元素x所在集合的大小:
int getSize(int x) {
int root = find(x);
return size[root];
}
2. 判断两个元素是否连通
bool isConnected(int x, int y) {
return find(x) == find(y);
}
3. 统计连通分量个数
遍历所有元素,统计根节点的数量(parent[i] == i的节点数):
int countComponents() {
int cnt = 0;
for (int i = 0; i < parent.size(); ++i) {
if (parent[i] == i) {
cnt++;
}
}
return cnt;
}
五、并查集的经典应用场景(附 C++ 示例)
场景 1:无向图的环检测
问题描述
原理
- 若
u和v已连通(find(u) == find(v)),则添加该边会形成环。
- 若未连通,则合并
u和v所在集合。
C++ 实现
#include <vector>
#include <iostream>
using namespace std;
class UnionFind {
private:
vector<int> parent;
vector<int> size;
public:
UnionFind(int n) {
parent.resize(n);
size.resize(n, 1);
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
int root = x;
while (parent[root] != root) root = parent[root];
while (parent[x] != root) {
int next = parent[x];
parent[x] = root;
x = next;
}
return root;
}
void unite(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return;
if (size[rootX] < size[rootY]) {
parent[rootX] = rootY;
size[rootY] += size[rootX];
} else {
parent[rootY] = rootX;
size[rootX] += size[rootY];
}
}
bool isConnected(int x, int y) {
return find(x) == find(y);
}
};
bool hasCycle(int n, vector<vector<int>>& edges) {
UnionFind uf(n);
for (auto& edge : edges) {
int u = edge[0], v = edge[1];
if (uf.isConnected(u, v)) {
return true;
}
uf.unite(u, v);
}
return false;
}
int main() {
int n = 5;
vector<vector<int>> edges = {{0,1}, {1,2}, {2,3}, {3,1}};
cout << "是否有环:" << (hasCycle(n, edges) ? "是" : "否") << endl;
return 0;
}
场景 2:连通分量计数(岛屿数量变种)
问题描述
给定一个m×n的网格,1 表示陆地,0 表示水域,统计连通的陆地块数(上下左右为连通)。
原理
将每个陆地格子视为一个元素,编号为i×n + j(二维转一维),遍历每个陆地格子,与相邻陆地格子合并,最后统计连通分量个数。
C++ 实现
#include <vector>
#include <iostream>
using namespace std;
class UnionFind {
private:
vector<int> parent;
vector<int> size;
int count;
public:
UnionFind(int n) {
parent.resize(n);
size.resize(n, 1);
count = n;
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return;
if (size[rootX] < size[rootY]) {
parent[rootX] = rootY;
size[rootY] += size[rootX];
} else {
parent[rootY] = rootX;
size[rootX] += size[rootY];
}
count--;
}
int getCount() {
return count;
}
};
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int m = grid.size(), n = grid[0].size();
int waterCount = 0;
UnionFind uf(m * n);
int dirs[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '0') {
waterCount++;
continue;
}
for (auto& dir : dirs) {
int x = i + dir[0], y = j + dir[1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
int idx1 = i * n + j;
int idx2 = x * n + y;
uf.unite(idx1, idx2);
}
}
}
}
return uf.getCount() - waterCount;
}
int main() {
vector<vector<char>> grid = {{'1','1','0','0','0'}, {'1','1','0','0','0'}, {'0','0','1','0','0'}, {'0','0','0','1','1'}};
cout << "岛屿数量:" << numIslands(grid) << endl;
return 0;
}
场景 3:Kruskal 算法求最小生成树
原理
Kruskal 算法的核心是'选边不形成环',利用并查集快速判断边的两个端点是否连通,优先选择权值最小的边合并,最终得到最小生成树。
六、带权并查集与扩展域并查集
1. 带权并查集(Weighted Union-Find)
核心思想
在parent[]基础上,新增weight[]数组,存储节点到父节点的权值(如距离、偏移量、关系系数)。find(x)时不仅压缩路径,还需更新权值(维护路径上的权值累加);unite(x, y)时需根据已知关系计算根节点间的权值,确保权值一致性。
应用场景
- 维护节点间的相对关系(如'x 比 y 大 5''x 是 y 的前驱')。
- 解决动态连通性中的权值传递问题(如 LeetCode 399. 除法求值)。
C++ 简化示例(维护节点到根的距离)
class WeightedUnionFind {
private:
vector<int> parent;
vector<int> weight;
public:
WeightedUnionFind(int n) {
parent.resize(n);
weight.resize(n, 0);
for (int i = 0; i < n; ++i) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) {
int origParent = parent[x];
parent[x] = find(parent[x]);
weight[x] += weight[origParent];
}
return parent[x];
}
void unite(int x, int y, int d) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return;
parent[rootX] = rootY;
weight[rootX] = d + weight[y] - weight[x];
}
int getDistance(int x, int y) {
if (find(x) != find(y)) return -1;
return weight[x] - weight[y];
}
};
2. 扩展域并查集(Extended Union-Find)
核心思想
将每个元素的'状态'扩展为多个节点(如'x 的朋友''x 的敌人'),通过合并扩展节点间接维护元素间的复杂关系(如'敌人的敌人是朋友')。
应用场景
- 处理对立关系(如 LeetCode 886. 可能的二分法)。
- 解决集合中的'黑白染色'问题。
核心思路
- 每个元素
x拆分为两个节点:x(表示'x 属于 A 组')和x + n(表示'x 属于 B 组')。
- 若
x和y是敌人,则合并x与y + n、x + n与y(x 在 A 则 y 在 B,x 在 B 则 y 在 A)。
- 若
x和y是朋友,则合并x与y、x + n与y + n(x 和 y 同组)。
七、时间复杂度与空间复杂度分析
1. 时间复杂度
- 朴素版:
find和unite均为O(n)(树退化为链表)。
- 优化版(路径压缩 + 按秩/大小合并):单次操作时间复杂度为
O(α(n)),α是阿克曼函数的反函数,增长极慢(n≤1e6时α(n)=4),可视为O(1)。
- 带权/扩展域并查集:时间复杂度与优化版一致,仅增加常数级的权值计算/扩展节点操作。
2. 空间复杂度
- 基础版:
O(n)(parent、rank/size数组)。
- 带权版:
O(n)(新增weight数组)。
- 扩展域版:
O(kn)(k为扩展状态数,如敌人/朋友场景k=2)。
注意事项
1. 核心要点
- 并查集的灵魂是「路径压缩」和「按秩/大小合并」,缺一不可,否则效率会急剧下降。
- 优先使用循环版
find(避免递归栈溢出,尤其当n较大时)。
- 二维网格问题需将二维坐标转为一维编号(如
i×n + j),方便并查集管理。
2. 常见误区
- 合并时直接操作元素
x和y,而非它们的根节点(会导致合并失效)。
- 路径压缩时忘记更新带权并查集的权值(导致权值错误)。
- 扩展域并查集的状态拆分逻辑错误(如敌人关系合并方向颠倒)。
3. 适用场景总结
- 动态连通性查询(如好友关系、网络连通)。
- 集合合并与计数(如连通分量、岛屿数量)。
- 无向图环检测(如 Kruskal 算法)。
- 节点间关系维护(带权/扩展域并查集)。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online