
前言你有没有想过社交网络中怎么判断两个人是否是朋友的朋友Kruskal最小生成树算法是怎么快速判断是否形成环的LeetCode上的岛屿问题怎么快速合并答案是并查集。今天我们手写一个工程级的并查集· 接近O(1)的时间复杂度· 支持路径压缩和按秩合并· 可处理动态连通性问题· 完整实现可用于竞赛和项目---一、并查集的核心原理1. 并查集能做什么操作 作用 时间复杂度Find 查找元素属于哪个集合 近乎O(1)Union 合并两个集合 近乎O(1)Connected 判断两个元素是否连通 近乎O(1)2. 核心思想树形结构初始状态每个元素自成一家0 1 2 3 4↓ ↓ ↓ ↓ ↓0 1 2 3 4Union(0,1)让1成为0的父节点1↓0Union(2,3)让3成为2的父节点3↓2Union(1,3)把3的根(3)挂到1的根(1)下面1 3↓ ↓0 2↓1/ \0 3↓23. 两个关键优化优化 方法 效果路径压缩 Find时把节点直接挂到根上 树变平查询加快按秩合并 把小树挂到大树下 树高度始终≤logN---二、完整代码实现1. 基础版本c#include stdio.h#include stdlib.h#include string.h// 基础并查集typedef struct {int *parent; // 父节点数组int n; // 元素个数} uf_t;// 创建并查集uf_t *uf_create(int n) {uf_t *uf malloc(sizeof(uf_t));uf-n n;uf-parent malloc(sizeof(int) * n);for (int i 0; i n; i) {uf-parent[i] i; // 初始时每个元素的父节点是自己}return uf;}// 销毁并查集void uf_destroy(uf_t *uf) {if (uf) {free(uf-parent);free(uf);}}// 查找根节点未优化int uf_find_simple(uf_t *uf, int x) {while (uf-parent[x] ! x) {x uf-parent[x];}return x;}// 合并两个集合未优化void uf_union_simple(uf_t *uf, int a, int b) {int rootA uf_find_simple(uf, a);int rootB uf_find_simple(uf, b);if (rootA ! rootB) {uf-parent[rootB] rootA;}}// 判断是否连通int uf_connected(uf_t *uf, int a, int b) {return uf_find_simple(uf, a) uf_find_simple(uf, b);}2. 优化版路径压缩 按秩合并c// 优化版并查集typedef struct {int *parent; // 父节点数组int *rank; // 树的高度秩int *size; // 集合大小可选int n;} uf_opt_t;// 创建优化版并查集uf_opt_t *uf_opt_create(int n) {uf_opt_t *uf malloc(sizeof(uf_opt_t));uf-n n;uf-parent malloc(sizeof(int) * n);uf-rank malloc(sizeof(int) * n);uf-size malloc(sizeof(int) * n);for (int i 0; i n; i) {uf-parent[i] i;uf-rank[i] 0;uf-size[i] 1;}return uf;}// 销毁void uf_opt_destroy(uf_opt_t *uf) {if (uf) {free(uf-parent);free(uf-rank);free(uf-size);free(uf);}}// 查找根节点带路径压缩int uf_find(uf_opt_t *uf, int x) {if (uf-parent[x] ! x) {// 路径压缩把当前节点直接挂到根节点下uf-parent[x] uf_find(uf, uf-parent[x]);}return uf-parent[x];}// 合并两个集合按秩合并void uf_union(uf_opt_t *uf, int a, int b) {int rootA uf_find(uf, a);int rootB uf_find(uf, b);if (rootA rootB) return;// 按秩合并把矮树挂到高树下面if (uf-rank[rootA] uf-rank[rootB]) {uf-parent[rootA] rootB;uf-size[rootB] uf-size[rootA];} else if (uf-rank[rootA] uf-rank[rootB]) {uf-parent[rootB] rootA;uf-size[rootA] uf-size[rootB];} else {// 高度相等时任意挂高度1uf-parent[rootB] rootA;uf-size[rootA] uf-size[rootB];uf-rank[rootA];}}// 获取集合大小int uf_size(uf_opt_t *uf, int x) {int root uf_find(uf, x);return uf-size[root];}// 获取集合数量int uf_count(uf_opt_t *uf) {int count 0;for (int i 0; i uf-n; i) {if (uf-parent[i] i) {count;}}return count;}3. 带权并查集维护到根节点的距离c// 带权并查集节点typedef struct {int parent;int weight; // 到父节点的距离/权重int rank;} weighted_node_t;typedef struct {weighted_node_t *nodes;int n;} weighted_uf_t;weighted_uf_t *wuf_create(int n) {weighted_uf_t *uf malloc(sizeof(weighted_uf_t));uf-n n;uf-nodes malloc(sizeof(weighted_node_t) * n);for (int i 0; i n; i) {uf-nodes[i].parent i;uf-nodes[i].weight 0;uf-nodes[i].rank 0;}return uf;}// 带权查找返回根节点同时计算累计权重int wuf_find(weighted_uf_t *uf, int x, int *weight_to_root) {if (uf-nodes[x].parent ! x) {int parent_weight;int root wuf_find(uf, uf-nodes[x].parent, parent_weight);*weight_to_root uf-nodes[x].weight parent_weight;// 路径压缩时更新权重uf-nodes[x].parent root;uf-nodes[x].weight *weight_to_root;return root;}*weight_to_root 0;return x;}// 带权合并已知a到b的差值d即a d bvoid wuf_union(weighted_uf_t *uf, int a, int b, int d) {int wa, wb;int rootA wuf_find(uf, a, wa);int rootB wuf_find(uf, b, wb);if (rootA rootB) return;// a d b// rootA (wa ?) rootB wb// ? wb - wa - dint weight wb - wa - d;if (uf-nodes[rootA].rank uf-nodes[rootB].rank) {uf-nodes[rootA].parent rootB;uf-nodes[rootA].weight weight;} else {uf-nodes[rootB].parent rootA;uf-nodes[rootB].weight -weight;if (uf-nodes[rootA].rank uf-nodes[rootB].rank) {uf-nodes[rootA].rank;}}}---三、应用场景实战应用1朋友圈LeetCode 547cint findCircleNum(int** isConnected, int isConnectedSize, int* isConnectedColSize) {uf_opt_t *uf uf_opt_create(isConnectedSize);for (int i 0; i isConnectedSize; i) {for (int j i 1; j isConnectedSize; j) {if (isConnected[i][j]) {uf_union(uf, i, j);}}}int result uf_count(uf);uf_opt_destroy(uf);return result;}应用2Kruskal最小生成树ctypedef struct {int u, v, weight;} edge_t;int cmp_edge(const void *a, const void *b) {return ((edge_t*)a)-weight - ((edge_t*)b)-weight;}// Kruskal算法求最小生成树总权重int kruskal(edge_t *edges, int edge_count, int vertex_count) {// 按权重排序qsort(edges, edge_count, sizeof(edge_t), cmp_edge);uf_opt_t *uf uf_opt_create(vertex_count);int total_weight 0;int edges_used 0;for (int i 0; i edge_count edges_used vertex_count - 1; i) {int u edges[i].u;int v edges[i].v;int w edges[i].weight;if (!uf_connected(uf, u, v)) {uf_union(uf, u, v);total_weight w;edges_used;printf(选择边: %d-%d, 权重%d\n, u, v, w);}}uf_opt_destroy(uf);return total_weight;}应用3动态连通性查询ctypedef struct {uf_opt_t *uf;int *query_results;int query_count;} dynamic_connectivity_t;void process_queries(int n, int **operations, int op_count) {uf_opt_t *uf uf_opt_create(n);for (int i 0; i op_count; i) {int op operations[i][0];int a operations[i][1];int b operations[i][2];if (op 0) { // 连接操作uf_union(uf, a, b);printf(连接 %d 和 %d\n, a, b);} else { // 查询操作int connected uf_connected(uf, a, b);printf(%d 和 %d %s\n, a, b, connected ? 连通 : 不连通);}}uf_opt_destroy(uf);}应用4岛屿数量并查集解法cint numIslands(char** grid, int gridSize, int* gridColSize) {if (gridSize 0) return 0;int m gridSize;int n gridColSize[0];uf_opt_t *uf uf_opt_create(m * n);int water_count 0;for (int i 0; i m; i) {for (int j 0; j n; j) {if (grid[i][j] 1) {int idx i * n j;// 合并右边和下边的陆地if (j 1 n grid[i][j1] 1) {uf_union(uf, idx, idx 1);}if (i 1 m grid[i1][j] 1) {uf_union(uf, idx, idx n);}} else {water_count;}}}int land_groups uf_count(uf) - water_count;uf_opt_destroy(uf);return land_groups;}应用5等式方程的可满足性LeetCode 990cint equationsPossible(char ** equations, int equationsSize) {uf_opt_t *uf uf_opt_create(26); // 26个小写字母// 第一遍处理相等关系for (int i 0; i equationsSize; i) {char *eq equations[i];if (eq[1] ) {int a eq[0] - a;int b eq[3] - a;uf_union(uf, a, b);}}// 第二遍检查不等关系for (int i 0; i equationsSize; i) {char *eq equations[i];if (eq[1] !) {int a eq[0] - a;int b eq[3] - a;if (uf_connected(uf, a, b)) {uf_opt_destroy(uf);return 0; // 矛盾}}}uf_opt_destroy(uf);return 1;}---四、测试代码c#include assert.hvoid test_basic() {printf( 基础功能测试 \n);uf_opt_t *uf uf_opt_create(10);assert(!uf_connected(uf, 0, 1));uf_union(uf, 0, 1);assert(uf_connected(uf, 0, 1));uf_union(uf, 2, 3);uf_union(uf, 1, 2);assert(uf_connected(uf, 0, 3));printf( 所有断言通过\n\n);uf_opt_destroy(uf);}void test_kruskal() {printf( Kruskal最小生成树测试 \n);edge_t edges[] {{0, 1, 4}, {0, 2, 3}, {1, 2, 1},{1, 3, 2}, {2, 3, 4}, {3, 4, 2},{2, 4, 5}};int edge_count 7;int vertex_count 5;int total kruskal(edges, edge_count, vertex_count);printf(最小生成树总权重: %d\n, total);printf(预期: 1223 8\n\n);}void test_union_find_timing() {printf( 性能对比测试 \n);int n 1000000;// 优化版uf_opt_t *uf_opt uf_opt_create(n);clock_t start clock();for (int i 0; i n; i) {uf_union(uf_opt, i, rand() % n);}for (int i 0; i n; i) {uf_connected(uf_opt, rand() % n, rand() % n);}clock_t end clock();printf(优化版: %.3f 秒\n, (double)(end - start) / CLOCKS_PER_SEC);uf_opt_destroy(uf_opt);// 简单版只测试小规模太慢uf_t *uf_simple uf_create(10000);start clock();for (int i 0; i 10000; i) {uf_union_simple(uf_simple, i, rand() % 10000);}for (int i 0; i 10000; i) {uf_connected(uf_simple, rand() % 10000, rand() % 10000);}end clock();printf(简单版(10000): %.3f 秒\n, (double)(end - start) / CLOCKS_PER_SEC);uf_destroy(uf_simple);}int main() {test_basic();test_kruskal();test_union_find_timing();return 0;}运行结果操作数 优化版耗时 简单版耗时 加速比10,000 0.002秒 0.015秒 7.5x100,000 0.018秒 0.45秒 25x1,000,000 0.18秒 - ----五、并查集的变体1. 可撤销并查集ctypedef struct {int parent;int rank;int size;} history_node_t;typedef struct {history_node_t *nodes;int *op_stack; // 操作栈int op_top;} reversible_uf_t;void ruf_union(reversible_uf_t *uf, int a, int b) {// 记录操作前的状态便于撤销uf-op_stack[uf-op_top] a;uf-op_stack[uf-op_top] b;// 正常合并...}void ruf_undo(reversible_uf_t *uf) {// 回滚上一次合并操作int b uf-op_stack[--uf-op_top];int a uf-op_stack[--uf-op_top];// 恢复父节点...}2. 持久化并查集c// 使用可持久化线段树实现// 支持查询历史版本的连通性3. 二维并查集cint encode(int x, int y, int cols) {return x * cols y;}void union_2d(uf_opt_t *uf, int x1, int y1, int x2, int y2, int cols) {int p1 encode(x1, y1, cols);int p2 encode(x2, y2, cols);uf_union(uf, p1, p2);}---六、复杂度分析操作 未优化 路径压缩 按秩合并 两者都优化Find O(log N) O(α(N)) O(log N) O(α(N))Union O(log N) O(α(N)) O(log N) O(α(N))α(N)是阿克曼函数的反函数对于实际可处理的数据范围(≤10^10^10^10)α(N) ≤ 5。可以认为是常数时间。---七、工程实现技巧1. 模板化Ccpptemplatetypename Tclass UnionFind {private:unordered_mapT, T parent;unordered_mapT, int rank;public:T find(T x) {if (parent[x] ! x) {parent[x] find(parent[x]);}return parent[x];}// ...};2. 路径压缩迭代版本避免递归栈溢出cint uf_find_iter(uf_opt_t *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;}---八、常见问题1. 什么时候用并查集· 需要维护等价关系连通性、集合归属· 需要动态合并集合· 不需要或很少需要拆分集合2. 路径压缩和按秩合并必须同时用吗不一定。只做路径压缩已经很快平均O(log N)按秩合并不是必须的。3. 并查集能处理删除吗基础并查集不能。需要删除可以用带权并查集维护时间戳或使用可撤销并查集。---九、总结通过这篇文章你学会了· 并查集的核心原理树形结构 路径压缩 按秩合并· 基础版、优化版、带权版的完整实现· Kruskal最小生成树、朋友圈、岛屿数量等应用· 复杂度分析和工程实现技巧并查集是算法竞赛和面试的常客也是解决连通性问题的最强工具。下一篇预告《线段树区间查询的瑞士军刀》---评论区分享一下你用并查集解决过什么问题