并查集的优化
- 导读
- 一、合并优化
- 1.1 基本原理
- 1.2 按大小合并
- 1.3 按秩合并
- 1.4 两种合并的区别
- **1.4.1 核心目标**
- **1.4.2 数据存储**
- **1.4.3 合并逻辑**
- **1.4.4 树高控制**
- **1.4.5 适用场景**
- **1.4.6 路径压缩兼容性**
- **1.4.7 极端案例对比**
- **1.4.8 小结**
- 二、查找优化
- 2.1 路径压缩
- 2.2 算法实现
- 三、算法评估
- 3.1**时间复杂度**
- 3.1.1 **按大小合并(Union by Size)**
- 3.1.2 **按秩合并(Union by Rank)**
- 3.1.3 **路径压缩(Path Compression)**
- 3.2 **空间复杂度**
- 3.2.1 **按大小合并**
- 3.2.2 **按秩合并**
- 3.2.3 **路径压缩**
- 3.3 **优化策略的协同作用**
- 3.3.1 **按大小合并 + 路径压缩**
- 3.3.2 **按秩合并 + 路径压缩**
- 3.4 **极端场景示例**
- 3.4.1 **按大小合并**
- 3.4.2 **按秩合并**
- 3.5 小结
- 🚀 结语:并查集优化——效率与智慧的结晶
- 🔍 **核心优化回顾**
- ⏱️ **时间复杂度突破**
- 🧩 **优化策略对比与选择**
- 💡 **实战启示**
- 🌟 **下篇预告**
导读
大家好,很高兴又和大家见面啦!!!
在上一篇内容中我们正确认识了并查集,并通过数据元素与其双亲指针的映射关系实现了并查集的查找与合并的。
但是上一篇的算法实现中,算法的最坏时间复杂度可以达到 O ( N ) O(N) O(N),这并不能很好的满足高效处理动态连通性问题。
在今天的内容中,我们将通过路径优化与按秩合并两种优化方式对并查集实现进一步的优化,大大提高处理动态连通性的效率。
一、合并优化
在并查集中,影响其时间复杂度的操作在查找上,而树的高度h是影响查找时间复杂度的重要因素。那么我们要对并查集的算法进行优化,那就得从树高下手。
下面我们就来看一下如何通过优化不同子集的合并从而优化整个并查集的算法。
1.1 基本原理
当两个树高不同的子集进行合并时,会存在两种合并方案:
- 小树合并到大树
- 大树合并到小树
不难发现,当大树将小树合并时并不会影响大树的树高,但是当小树将大树合并时,则会增加树高。
试想一下,如果我们要完成n棵树的合并,如果将n-1棵小树合并到大树上,这样我们就能保证在进行查找时,能够尽可能的降低查找时间复杂度。
但是当我们在进行合并时,是将大树合并到小树上,则会不断地增加树高,这样每一次合并,都会增加查找的时间复杂度。
因此为了尽可能优化并查集,那么我们在进行合并操作时,应该在每一次合并操作时,都保证是小树合并到大树上。
今天我们介绍两种优化方案——按大小合并、按秩合并。
1.2 按大小合并
按大小合并指的是根据树的大小,将小树合并到大树。
树的大小由树的结点数量决定,结点多的树为大树,结点少的树为小数。通过树的根结点的绝对值表示树的大小。
这里我们以大写字符为例,我们通过一个整型数组parent来存放每个元素的双亲位置信息:
该算法的实现比较简单,我们只需要在合并算法中加入一个对根结点大小的判断,并且在完成合并后,修改根结点的值:
//按大小合并优化
void Union_by_Size(DSU* s, int root1, int root2) {if (root1 != root2) {//根结点为负数,值越大,结点数量越少if (s->parent[root1] > s->parent[root2]) {s->parent[root2] += s->parent[root1];//修改大树根结点s->parent[root1] = root2;//修改小树根结点}else {s->parent[root1] += s->parent[root2];s->parent[root2] = root1;}}
}
通过按大小合并的方式优化,可以极大可能的降低合并后的树高,但是也不一定。
比如结点多但高度低的大树与结点少但高度高的小树进行合并,此时如果按照大小来进行合并,则会增加合并后树的高度。
那有没有不会影响高度的优化方式呢?这就是我们接下来要介绍的按秩合并;
1.3 按秩合并
所谓的秩(Rank)代表的是树合并前的最大高度。
按秩合并实际上就是根据树高进行合并,将矮树合并到高树中,从而确保在完成合并后的树高最小。
合并的规则如下:
- 所有结点的初始高度为0,即根结点的高度为0
- 将秩小的树合并到秩大的树下,秩保持不变
- 将秩相同的两棵树进行合并,合并后,根结点的秩加1
在按秩合并中,我们需要维护两个数组:
- parent数组——记录结点的双亲位置
- rank数组——记录根结点的秩
对应的实现也很简单,如下所示:
//按秩合并
void Union_by_Rank(DSU* s, int root1, int root2) {if (root1 != root2) {if (s->rank[root1] > s->rank[root2]) {s->parent[root2] = root1;//低秩合并到高秩中}else {s->parent[root1] = root2;if (s->rank[root1] == s->rank[root2]) {s->rank[root2]