【数据结构与算法】第42篇:并查集(Disjoint Set Union)

目录

一、并查集的基本概念

[1.1 什么是并查集](#1.1 什么是并查集)

[1.2 核心思想](#1.2 核心思想)

二、基础实现

[2.1 结构定义](#2.1 结构定义)

[2.2 初始化](#2.2 初始化)

[2.3 查找(Find)](#2.3 查找(Find))

[2.4 合并(Union)](#2.4 合并(Union))

三、优化:路径压缩

[3.1 问题分析](#3.1 问题分析)

[3.2 路径压缩](#3.2 路径压缩)

四、优化:按秩合并

[4.1 问题分析](#4.1 问题分析)

[4.2 按秩合并](#4.2 按秩合并)

五、完整代码实现

六、经典应用:朋友圈问题

[6.1 问题描述](#6.1 问题描述)

[6.2 代码实现](#6.2 代码实现)

七、Kruskal算法中的并查集

八、复杂度分析

九、路径压缩的可视化

十、小结

十一、思考题


一、并查集的基本概念

1.1 什么是并查集

并查集(Union-Find)维护一组不相交的集合,支持:

  • 查找(Find):确定元素属于哪个集合

  • 合并(Union):将两个集合合并为一个

应用场景

场景 说明
朋友圈问题 判断两人是否在同一社交圈
Kruskal算法 判断添加边是否会形成环
连通分量 统计图中连通块数量
动态连通性 网络连接、道路连通

1.2 核心思想

表示集合,树的根节点代表这个集合。每个元素存储其父节点,根节点的父节点指向自己。

text

复制代码
初始:每个元素独立
0    1    2    3    4

合并{0,2}后:
    0
    |
    2

合并{1,3}后:
    1
    |
    3

合并{2,3}后(合并两个集合):
        0
       / \
      2   1
          |
          3

二、基础实现

2.1 结构定义

c

复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 1000

typedef struct {
    int parent[MAX_SIZE];  // 父节点数组
    int rank[MAX_SIZE];    // 树的高度(用于优化)
} UnionFind;

2.2 初始化

每个元素自成一个集合,父节点指向自己,秩为0。

c

复制代码
void ufInit(UnionFind *uf, int n) {
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
        uf->rank[i] = 0;
    }
}

2.3 查找(Find)

找到根节点(父节点指向自己的节点)。

c

复制代码
int ufFind(UnionFind *uf, int x) {
    while (uf->parent[x] != x) {
        x = uf->parent[x];
    }
    return x;
}

2.4 合并(Union)

将两个集合的根节点连接,把一棵树的根指向另一棵树的根。

c

复制代码
void ufUnion(UnionFind *uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);
    
    if (rootX != rootY) {
        uf->parent[rootY] = rootX;
    }
}

三、优化:路径压缩

3.1 问题分析

基础版本的Find在最坏情况下会退化成链表,查找复杂度O(n)。

text

复制代码
合并顺序:0-1, 1-2, 2-3, 3-4
树结构:0 → 1 → 2 → 3 → 4
查找4需要遍历5次

3.2 路径压缩

在查找过程中,将路径上的所有节点直接指向根节点。

c

复制代码
// 递归写法(更简洁)
int ufFind(UnionFind *uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = ufFind(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

// 非递归写法
int ufFind(UnionFind *uf, int x) {
    int root = x;
    while (uf->parent[root] != root) {
        root = uf->parent[root];
    }
    // 路径压缩
    while (x != root) {
        int next = uf->parent[x];
        uf->parent[x] = root;
        x = next;
    }
    return root;
}

四、优化:按秩合并

4.1 问题分析

路径压缩已经很快,但Union时如果随意连接,可能导致树高度增加。

4.2 按秩合并

将高度小的树接到高度大的树下,避免树变高。

c

复制代码
void ufUnion(UnionFind *uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);
    
    if (rootX == rootY) return;
    
    // 按秩合并:高度小的接在高度大的下面
    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
}

五、完整代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 1000

typedef struct {
    int parent[MAX_SIZE];
    int rank[MAX_SIZE];
} UnionFind;

// 初始化
void ufInit(UnionFind *uf, int n) {
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
        uf->rank[i] = 0;
    }
}

// 查找(路径压缩)
int ufFind(UnionFind *uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = ufFind(uf, uf->parent[x]);
    }
    return uf->parent[x];
}

// 合并(按秩合并)
void ufUnion(UnionFind *uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);
    
    if (rootX == rootY) return;
    
    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
}

// 判断两个元素是否在同一集合
int ufConnected(UnionFind *uf, int x, int y) {
    return ufFind(uf, x) == ufFind(uf, y);
}

// 统计集合数量
int ufCount(UnionFind *uf, int n) {
    int count = 0;
    for (int i = 0; i < n; i++) {
        if (uf->parent[i] == i) count++;
    }
    return count;
}

// 打印每个元素所属的根
void printSets(UnionFind *uf, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d -> %d\n", i, ufFind(uf, i));
    }
}

int main() {
    UnionFind uf;
    int n = 10;
    ufInit(&uf, n);
    
    printf("=== 初始状态 ===\n");
    printf("集合数量: %d\n", ufCount(&uf, n));
    
    // 合并操作
    ufUnion(&uf, 0, 1);
    ufUnion(&uf, 2, 3);
    ufUnion(&uf, 0, 2);
    ufUnion(&uf, 4, 5);
    ufUnion(&uf, 6, 7);
    ufUnion(&uf, 8, 9);
    ufUnion(&uf, 5, 6);
    
    printf("\n=== 合并后 ===\n");
    printf("集合数量: %d\n", ufCount(&uf, n));
    printf("\n元素关系:\n");
    printSets(&uf, n);
    
    printf("\n=== 连通性查询 ===\n");
    printf("0和3是否连通: %s\n", ufConnected(&uf, 0, 3) ? "是" : "否");
    printf("4和7是否连通: %s\n", ufConnected(&uf, 4, 7) ? "是" : "否");
    printf("4和8是否连通: %s\n", ufConnected(&uf, 4, 8) ? "是" : "否");
    
    return 0;
}

运行结果:

text

复制代码
=== 初始状态 ===
集合数量: 10

=== 合并后 ===
集合数量: 3

元素关系:
0 -> 0
1 -> 0
2 -> 0
3 -> 0
4 -> 4
5 -> 4
6 -> 4
7 -> 4
8 -> 8
9 -> 8

=== 连通性查询 ===
0和3是否连通: 是
4和7是否连通: 是
4和8是否连通: 否

六、经典应用:朋友圈问题

6.1 问题描述

有n个人,给定m对好友关系(互为好友),求有多少个朋友圈(连通分量)。

6.2 代码实现

c

复制代码
// 统计朋友圈数量
int friendCircles(int n, int edges[][2], int edgeCount) {
    UnionFind uf;
    ufInit(&uf, n);
    
    for (int i = 0; i < edgeCount; i++) {
        ufUnion(&uf, edges[i][0], edges[i][1]);
    }
    
    return ufCount(&uf, n);
}

int main() {
    // 5个人,好友关系:0-1, 1-2, 3-4
    int edges[][2] = {{0, 1}, {1, 2}, {3, 4}};
    int n = 5;
    int edgeCount = sizeof(edges) / sizeof(edges[0]);
    
    int circles = friendCircles(n, edges, edgeCount);
    printf("朋友圈数量: %d\n", circles);
    
    return 0;
}

运行结果:

text

复制代码
朋友圈数量: 2

七、Kruskal算法中的并查集

c

复制代码
// Kruskal算法中判断边是否会形成环
typedef struct {
    int u, v, weight;
} Edge;

int kruskalMST(Edge edges[], int n, int edgeCount) {
    // 按权值排序
    qsort(edges, edgeCount, sizeof(Edge), cmpEdge);
    
    UnionFind uf;
    ufInit(&uf, n);
    
    int totalWeight = 0;
    int selectedEdges = 0;
    
    for (int i = 0; i < edgeCount && selectedEdges < n - 1; i++) {
        int u = edges[i].u;
        int v = edges[i].v;
        
        // 如果不在同一集合,加入MST
        if (ufFind(&uf, u) != ufFind(&uf, v)) {
            ufUnion(&uf, u, v);
            totalWeight += edges[i].weight;
            selectedEdges++;
        }
    }
    
    return totalWeight;
}

八、复杂度分析

操作 时间复杂度 说明
初始化 O(n) 遍历所有元素
查找(未优化) O(n) 可能退化成链表
查找(路径压缩) O(α(n)) 接近O(1)
合并(未优化) O(n) 需要先查找
合并(按秩+路径压缩) O(α(n)) 接近O(1)

α(n) 是阿克曼函数的反函数,增长极慢,n=10^80时 α(n)≤5


九、路径压缩的可视化

c

复制代码
// 演示路径压缩效果
void demoPathCompression() {
    UnionFind uf;
    ufInit(&uf, 5);
    
    // 构造一条链 0->1->2->3->4
    uf.parent[1] = 0;
    uf.parent[2] = 1;
    uf.parent[3] = 2;
    uf.parent[4] = 3;
    
    printf("压缩前:\n");
    for (int i = 0; i < 5; i++) {
        printf("parent[%d]=%d\n", i, uf.parent[i]);
    }
    
    // 查找4,触发路径压缩
    ufFind(&uf, 4);
    
    printf("\n压缩后:\n");
    for (int i = 0; i < 5; i++) {
        printf("parent[%d]=%d\n", i, uf.parent[i]);
    }
}

运行结果:

text

复制代码
压缩前:
parent[0]=0
parent[1]=0
parent[2]=1
parent[3]=2
parent[4]=3

压缩后:
parent[0]=0
parent[1]=0
parent[2]=0
parent[3]=0
parent[4]=0

十、小结

这一篇我们学习了并查集:

操作 实现 优化
初始化 parent[i]=i -
查找 找根节点 路径压缩
合并 根相连 按秩合并

核心代码

c

复制代码
int find(int x) {
    return parent[x] == x ? x : (parent[x] = find(parent[x]));
}

void union(int x, int y) {
    int rx = find(x), ry = find(y);
    if (rx != ry) parent[ry] = rx;
}

适用场景

  • 动态连通性判断

  • 最小生成树(Kruskal)

  • 朋友圈、岛屿数量

下一篇我们讲Trie树(前缀树)。


十一、思考题

  1. 路径压缩和按秩合并分别优化了什么?可以只用其中一个吗?

  2. 并查集的查找和合并操作的时间复杂度为什么是O(α(n))?

  3. 如何用并查集判断一个图是否有环?

  4. 在Kruskal算法中,为什么用并查集来判断环比DFS更高效?

欢迎在评论区讨论你的答案。

相关推荐
算法鑫探19 小时前
闰年判断:C语言实战解析
c语言·数据结构·算法·新人首发
WBluuue19 小时前
数据结构与算法:康托展开、约瑟夫环、完美洗牌
c++·算法
木子墨51620 小时前
LeetCode 热题 100 精讲 | 并查集篇:最长连续序列 · 岛屿数量 · 省份数量 · 冗余连接 · 等式方程的可满足性
数据结构·c++·算法·leetcode
浅时光_c20 小时前
14 结构体 共用体 枚举类型
c语言
2501_9219608521 小时前
双相自指图与弦论边界非对易性的结构同源
数据结构
王老师青少年编程21 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:均分纸牌
c++·算法·编程·贪心·csp·信奥赛·均分纸牌
EQUINOX121 小时前
2026年码蹄杯 本科院校赛道&青少年挑战赛道提高组初赛(省赛)第一场,个人题解
算法
萝卜小白21 小时前
算法实习Day04-MinerU2.5-pro
人工智能·算法·机器学习
Liangwei Lin21 小时前
洛谷 P3133 [USACO16JAN] Radio Contact G
数据结构·算法
weixin_5134499621 小时前
PCA、SVD 、 ICP 、kd-tree算法的简单整理总结
c++·人工智能·学习·算法·机器人