并查集:按秩归并&路径压缩

并查集是一种用于处理一些不相交集合的合并与查询的问题的数据结构。本文介绍了如何使用按秩归并优化并查集,避免树形结构高度增加导致查找效率下降。同时,通过路径压缩进一步提升查找效率,实现查找操作的线性时间复杂度。通过实例详细解释了按秩归并与路径压缩的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集合可以怎么表示?可以用一棵树来表示,结点表示集合的元素,而树根则用来代表这个集合。所以用树来做集合的并查集的话,对于查找某个元素属于哪个集合,我们就从这个结点开始往上找,找到它所在的这棵树的根结点。对于并集操作,只要把两棵树的根结点并在一起就可以了。所以为了满足这样的操作,我们的树结构有点小改变,变为双亲表示法“由孩子指向双亲。每个结点都向上指向它的父结点,而不是由父结点向下指向左右子树。”这样的树比较好的是用结构体数组来存储表示。

集合建立好后,接下来看查找操作:

函数把集合和要查找的元素传进去,然后第38for循环开始查找这个元素X在集合里的位置,也就是数组的下标。i=0开始,i小于MaxSize也就是数组的最大下标,并且S[ i ].Data!=X也就是没找到要查找的元素的话,就i++。循环做完之后,i就等于元素X在结构体数组中的下标。之和要继续找到这个元素结点所在树的根结点,因为根结点的值才是表示一个集合,我们要查找的是这个元素X属于哪一个集合。我们知道表示集合的结点的值是负数,且只有它是负数,所以接下来继续循环,如果S[ i ].Parent>=0的话,就表示没找到表示集合的结点,然后把S[ i ].Parent也就是当前结点的父结点的下标赋值给i,意思是往上一层查找,直到S[ i ].Parent<0了,代表找到根结点了,就退出循环,return i出去。

接着到集合的并运算。并运算是指给出两个元素的值,让你把这两个元素所在的集合并合在一起。这样操作我们要这样做:

1、首先要找到这两个元素所在的树的根结点(也就是找到这两个集合)。

2、判断这两个元素所在的集合是不是同一个集合,如果不是就做并运算,把其中一个树较矮的根结点指向另一个树较高的根结点的数组下标。


一开始先通过查找运算找出两个元素所在树的根结点,也就是找到各自所在的集合。如果两个集合不同,就比较哪个集合较高哪个集合较矮。找出较矮的哪个集合就把它的根结点的Parent换成较高树的根结点的数组下标,也就是让较矮的树指向了较高的树。

为什么要做这样一个查找操作?如果我们随便就把两棵树合并在一起,会出现怎样的一种情况?例如如果我们把一棵较高的树指向一棵较矮的树,那么合并后树的最大高度就会增加,随着并运算的操作次数越多,树变得越来越高,越往后那么集合的查找操作效率就会变得越来越低,因为查找操作是从要查找的结点元素开始一层一层往上找根结点,当集合树高度很大时,要遍历的层数就越多,效率就变得越低了。

所以为了解决这个问题,优化树变高后查找操作的效率,就要用到按秩归并。我们应该把较矮的树指向并到较高的树上(或者把规模小的树接到规模大的树上),这样较矮的树高度增加了,但较高的树高度没有增加,合并后的整棵树最大高度仍然是之前较高的树的高度,所以合并后树的最大高度并没有增加。


注意我们的代码,因为我们在集合的数据结构中,根结点的Parent是一个负数,且数值是表示这个树的所有结点的个数(比较高度时,数值为树的高度),所以比较两个集合(也就是比较两棵树的大小时),根结点的Parent较小的,树更高。第62行就是说当两棵树一样高时,就随便把一棵树并到另一棵树上,然后合并后的树高度加一。注意这里的树高度加一,在Parent上是做减减运算。

通过按秩归并对集合的并运算做优化后,程序运行就快了。其实我们可以继续做优化,用路径压缩。

首先我们看回上面的查找函数


39行,我们每次读入进来要查找的元素后,都要让i等于0开始,也就是从数组开头开始线性扫描整个数组,这样做的一个不足之处在于,假设数组有n个元素,我们有n个数要查找,而假设最坏情况下每次要查找的元素都是数组的最后一位也就是第n个,那么时间复杂度就会是n²。所以我们想,可以不可以把查找函数里的这一步线性查找做点优化?


我们看集合的表示数据结构,里面的数据域在数组的每个元素里都有一个Data来存它。但其实,任何一个有限的集合里的元素都可以被映射为从0n-1的整数。所以我们想,可以把元素的值直接用数组的下标来表示。这样就可以把集合的数据结构里的ElementType Data去掉了,也就简化了集合的数据结构,且这样做的好处在于,在查找函数里,我们不用做线性查找的步骤


我们把要查找的元素X传进来后,直接判断S[ X ].Parent是否小于0,如果是,那就是根结点找着了。否则,就递归调用Compress函数,把S[ X ].Parent传进去,也就是找X的父结点,往上一层继续找根结点。直到S[ X ].Parent<0找到后,就return X

我们用一个例子来解释下这个函数是怎么运作的:

例如对这样一个集合,我们要找的根结点是最上面粉红色的结点,在我们调用查找函数后,最后我们应该返回就是这个粉红色的结点。当我们第一次进入查找函数后,因为最底下的F结点不是我们要找的根结点,所以直接进入查找函数的第110行的递归往上找根结点


直到我们找到根结点后


就把根结点的X的返回出去,返回到上一步的return S[X].Parent=CompressFind(S, S[X].Parent);里,对于上一个结点来说,在返回之前,会先把根结点返回来的值赋给S[ X ].Parent,也就是把根结点变成父结点。之后再做renturn,而对于这个结点来说,return出去的正是自己的父结点,也就是整个集合的根结点。所以倒数第三个结点也指向了根结点


直到最后把根结点返回给了最初的F结点


这就是路径压缩,不仅在查找操作中找到了集合的根结点,同时把集合的路径压缩,这里的路径压缩指把每次扫过的结点都接上根结点上。这样做的好处在于,在做多次查找操作时,因为路径压缩了,所以会省去很多的时间。

 

 

并查集是一种用于管理集合的有效数据结构,主要用于处理一些不相交集合(Disjoint Sets)的合及查询问题。它支持两种基本操作:查找(Find)和联合(Union)。通过这两个操作可以方便地对一组元素进行动态分组。 ### 核心思想 1. **初始化**:最初给定n个单独的元素,每个元素都属于一个单元素集合。 2. **查找 (Find)** :确定某个特定元素所在的集合标识符,这通常是最上面的一个&ldquo;祖先&rdquo;节点。 3. **合 (Union)** : 把包含两个指定元素的集合归并成为一个新的集合。 为了提高效率,并查集中常常用到两种技术优化: #### 路径压缩(Path Compression) 在执行find的过程中同时修改沿途各结点的父亲指针使其直接指向根节点,这样以后再次访问这些节点时会更快找到它们所属的集合头部位置。 #### 按(Union by Rank) 当我们要union两个树的时候总是让较矮的一棵树链接到较高的那棵上去,以此尽量保持整个森林高度较低从而减少后续search的时间开销。 这两种技巧结合起来可以使几乎所有的实际应用达到近乎线性的运行时间性能O(n&alpha;(n)),这里&alpha;表示反阿克曼函数,在正常计算机范围内增长极慢接近于常数级别。 下面是一个简单的伪代码例子展示如何构建基础版的并查集: ```python class UnionFind: def __init__(self, size): self.parent = list(range(size)) # Find function with Path compression def find(self,x): if x != self.parent[x]: self.parent[x] = self.find(self.parent[x]) return self.parent[x] # Union Function using union-by-rank optimization can be added here ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值