路径压缩需递归回溯时统一更新父指针,仅改当前节点会导致后续查询路径未压缩;按秩合并比按大小更稳定,因rank维护树高上界且不随结构变化;初始化须parent[i]=i、rank[i]=0,三数组同长对齐;union前须判根相同并检查参数范围。

为什么直接写 find 时路径压缩不能只改一次父指针
常见错误是:在 find 中只把当前节点的 parent[x] 指向根,却不递归更新中间所有节点。结果是只有查询的那个节点被压平,下次查它的子节点时路径依然很长。
正确做法是「递归回溯时统一更新」:先找到根,再在函数返回途中把沿途每个节点的父指针都设为根。这样一次 find 就能压平整条链。
- 错误写法:
return parent[x] = find(parent[x]);—— 这其实是对的,但很多人误写成parent[x] = find(parent[x]); return parent[x];,后者没利用返回值更新自己 - 必须用引用或返回值链式赋值,否则中间节点父指针不变
- 非递归写法需额外栈存路径,容易漏掉某层更新
unionSet 里按秩合并比单纯看大小更稳
按大小合并(size)在某些场景下会退化,比如反复 union 大集合到小集合;而按秩合并(rank)维护的是树高上界,不随实际结构变化,更利于摊还分析。
实践中两者性能接近,但 rank 更易证明复杂度,且避免了 size 更新的额外开销(尤其做路径压缩后 size 不再准确)。
- 初始化时
rank[i] = 0,不是 1 - 仅当两棵树
rank相等时,才把一棵挂到另一棵下,并将根的rank+1 - 如果用
size,合并后要显式更新size[rootY] += size[rootX],别忘了这步
构造函数和成员变量怎么组织才不易出错
核心是三个数组/容器必须同长、同索引对齐:一个存父节点(parent),一个存秩或大小(rank 或 size),一个可选存连通分量数(count)。建议用 std::vector 而非裸数组,避免越界和析构问题。
- 构造函数参数应为节点总数
n,而非最大编号;节点编号默认从 0 到 n-1,减少边界判断 -
parent[i] = i初始化必须在构造时完成,漏掉会导致find(-1)等未定义行为 - 如果后续要支持动态加点,得换用
std::map,但会损失 O(1) 访问性能
测试时最容易忽略的边界情况
不是所有 unionSet(x, y) 都需要真正合并——当 x 和 y 已在同一集合时,find(x) == find(y) 应直接返回,否则重复 union 可能破坏 rank 或引发冗余操作。
另外,节点编号是否允许负数?是否允许超过初始化 n?这些在生产环境里常被忽略,但在竞赛或嵌入式场景中可能触发崩溃。
- 务必在
unionSet开头加if (rootX == rootY) return; - 对输入参数做范围检查(如
x >= 0 && x ),调试期打开,发布时可条件编译去掉 - 用小样例手模:3 个点,union(0,1), union(1,2),再查 find(0), find(2),确认都返回同一根且 parent 数组被压平
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











