并查集
并查集
一、什么是并查集?
并查集是一种简单的集合表示。
它支持以下 3 种操作:
Initial(S)
:将集合 S 中的所有元素初始化为一个个单元素集合Union(S, Root1, Root2)
:把集合 S 中的子集合 Root2 并入子集合 Root1 中Find(S, x)
:查找集合 S 中单元素 x 所在的子集合,并返回子集合的名字
通常用树表示每个子集合,子集合的合并就是树的合并。
子集合 Root2 合并到子集合 Root1,只需要把 Root2 的根节点指向 Root1 的根节点即可。
二、为什么要用并查集?
- 并查集主要用于解决一些元素分组的问题
- 并查集可用于管理一系列不相交的集合,执行集合的合并以及搜索元素所在集合
三、如何实现并查集?
3.1 集合初始化 Initial(S)
1 | void initial (int[] s) { |
初始化后单个元素就是一个集合
1 | 集合 S 存储: |
3.2 子集合合并 Union(S, Root1, Root2)
1 | void union(int[] s, int root1, int root2) { |
比如合并 0
和 1
得到子集合 0
:
1 | 集合 S 存储: |
再合并 3
和 4
得到子集合 3
:
1 | 集合 S 存储: |
还可以把子集合 0
和子集合 3
也合并起来:
1 | 集合 S 存储: |
3.3 查找子集合 Find(S, x)
1 | int find(int[] s, int x) { |
比如查找 4
所在集合时,过程是这样的:
1 | ___________________ _____ |
一直往上找,直到根节点为止,就能得到 4
所在的集合。
3.4 元素合并 Union(S, X1, X2)
判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
- 合并两个元素时,找到它们的根节点合并即可
所以上面的子集合合并代码可以改成这样:
1 | void union(int[] s, int x1, int x2) { |
四、路径压缩
简单的并查集,在某些场景下的性能是比较差的,例如:
- 链条变得越来越长的时候,查找根节点也变得越来越耗时
比如,下面查找元素 5 所在的子集合时,每次都需要经过好几个节点:
1 | ___ |
在并查集链比较长的时候,每次搜索元素的根节点,都比较耗时。
那怎么解决这个问题呢?
- 并查集中只关注根节点,因此可以采用路径压缩,将元素直接指向根节点
而实现的方法也很简单,就是:
- 在查询时,把路径上所有节点的父节点都指向根节点
代码上实现的话,可以采用递归,类似这样:
1 | int find(int[] s, int x) { |
相当于把上面的一条直链,变成了多叉树:
1 | ___ |
经过路径压缩后,就基本解决了链条过长引起的性能问题。
五、按秩合并
路径压缩虽然能解决查询时链条过长的问题,但是路径压缩是在查询时才做的。
- 当没有查询时,路径不会压缩,还是会出现链路很长的情况
- 每次查询时,只压缩一条路径,其他路径的链路还是很长
这种情况要如何避免呢?
在两个子集合进行合并时,只要尽量满足:
- 把简单的树往复杂的树上合并
就能避免并查集的树深度过大,影响查询的效率。
比如说,2 个子集合长这样:
1 | ___ ___ |
子集合的合并可分为 2 种情况:
3
合并到0
,树深度是还是2
,深度不变0
合并到3
,树深度变成了3
,深度加深了
所以肯定是把深度小的 3
合并到深度大的 0
更好一些,因为这样可以避免节点深度变得更大。
那怎么知道哪个集合的深度更高呢?
- 采用一个额外的数组,记录每个根节点的深度,合并子集合时同时更新
比如说,采用数组 rank
记录节点深度:
- 初始化时,把所有元素的
rank
(秩)设为默认值 1 - 合并子集合时,比较两个根节点,把
rank
较小合并到较大的上面
代码实现的话就类似这样:
初始化:
1 | void initial (int[] s, int[] rank) { |
合并子集合:
1 | void union(int[] s, int[] rank, int x1, int x2) { |
两棵树深度一样的时候,合并起来会加深一层:
1 | ___ ___ |
1 | ___ |
按秩合并和路径压缩一起用的时候,路径压缩会影响到 rank
值,导致它不准确。
虽然 rank
值不准确,但是不影响 2 者结合一起用。
总结
并查集是一种简单的集合表示,包括 3 种操作:
Initial(S)
:将集合 S 中的所有元素初始化为一个个单元素集合Union(S, Root1, Root2)
:把集合 S 中的子集合 Root2 并入子集合 Root1 中Find(S, x)
:查找集合 S 中单元素 x 所在的子集合,并返回子集合的名字
并查集的作用:
- 并查集主要用于解决一些元素分组的问题
- 并查集可用于管理一系列不相交的集合,执行集合的合并以及搜索元素所在集合
并查集有 2 种方法优化方式:
- 路径压缩:查询时压缩节点路径,直接指向根节点
- 按秩合并:合并时,深度小的合并到深度大的树上面
按秩合并和路径压缩一起用:
- 路径压缩会影响到按秩合并的
rank
值,导致它不准确 - 虽然
rank
值不准确了,但是不影响 2 者结合一起用
参考
《数据结构》王道考研系列
https://zhuanlan.zhihu.com/p/93647900
https://blog.csdn.net/weixin_38279101/article/details/112546053
附录
完整代码
1 | /** |