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

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

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

函数把集合和要查找的元素传进去,然后第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换成较高树的根结点的数组下标,也就是让较矮的树指向了较高的树。

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

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

扫描二维码关注公众号,回复: 2481012 查看本文章


注意我们的代码,因为我们在集合的数据结构中,根结点的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结点


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

猜你喜欢

转载自blog.csdn.net/justinzengtm/article/details/80726125