并查集的路径压缩优化

在上面两节中,我们讨论了基于Size和基于Rank的优化策略,但是这两种优化策略都集中在在优化Unionelements这个操作上,那么,对于并查集来说,是不是还有其他部分可以优化呢?当然,下面我们就来介绍一下优化find查询的优化策略:

一.基于循环的路径压缩优化


首先,让我们来回忆一下find执行的操作:从一个节点,不停的通过parent数组向上去寻找他的根节点,在这个过程中,我们相当于把从这个节点到根节点的这条路径上的所有的节点都给遍历了一遍,那么,让我们想一想,在find的同时,是否可以顺便加上一些其它的操作使得树的层数尽量变得更少呢?答案是可以的。

对于一个集合树来说,它的根节点下面可以依附着许多的节点,因此,我们可以尝试在find的过程中,从底向上,如果此时访问的节点不是根节点的话,那么我们可以把这个节点尽量的往上挪一挪,减少数的层数,这个过程就叫做路径压缩,下面举一个具体的例子来说明一下:

假设我们起始的并查集如上图所示,现在我们要find[4],首先我们根据parent[4]可以得出,4并不是一个根节点,因此,我们可以在向上继续查询之前,把这个节点往上面挪一挪(路径压缩),首先现在4节点连接到其父亲3节点上,我们可以让4节点不在指向3节点作为父亲节点了,而是让其跳一下,让其指向2节点(即父亲节点的父亲节点)作为新的父亲节点:(如果该元素的父亲节点正好是根节点,那么让其指向父亲节点的父亲节点并不会出错,因为根据根元素的父亲节点指向其自己的结构,此时父亲节点的父亲节点仍然是有效的,即还是根节点,不会发生越界问题

这样,我们就发现树的层数由原来的5层变成了现在的4层,即路径被压缩了一下。

下面,我们把继续来对2节点进行find操作,这里我们没有再去访问3节点,相当于跳过了一步操作(因为3节点也不是根节点,并不是我们想要返回的结果。如果3节点是根节点的话,那么4节点就会指向3节点,接下来就会访问3节点,所以这样的跳过是可行的),对于2节点来说,2节点也不是我们所要找到的根节点,因此,我们同样也可以对其进行压缩操作,让2节点指向父亲节点的父亲节点0节点作为新的父亲节点,如下图所示:

此时,树的层数由4层被压缩到了3层,与此同时,我们还跳过了一个1节点,接下来,我们只需要对0节点在进行路径压缩操作就好了。因为0节点是我们要找的根节点,因此,我们不在需要执行路径压缩操作了,只需要把找到的结果即根节点给返回就好了。

通过上面的过程我们可以看到,在进行find操作的同时,我们不仅把需要查找的根节点给找到了,与此同时我们还对树进行了压缩操作,这便是路径压缩的意思。通过路径压缩,我们在下一次执行find操作的时候,层数变得尽可能少了,那么效率将会大大的提高。

下面我们来看一下具体的代码实现:

新的find实现方法:

 

//找出元素p位于的集合的编号,同时进行路径压缩操作
        //即返回根元素
        int find(int p){
            assert(p>=0&&p<count);//防止数组越界
            while(p != parent[p]){//如果p元素的父亲指针指向的不是自己,说明p并不是集合中的根元素,还需要一直向上查找和路径压缩
                //在find查询中嵌入一个路径压缩操作
                parent[p]=parent[parent[p]];
                //p元素不再选择原来的父亲节点,而是直接选择父亲节点的父亲节点来做为自己新的一个父亲节点
                //这样的操作使得树的层数被压缩了
                p=parent[p];//p压缩完毕后且p并不是根节点,p变成p新的父节点继续进行查找和压缩的同时操作
            }
            return p;//经过while循环后,p=parent[p],一定是一个根节点,且不能够再进行压缩了,我们返回即可
        }

我们前面讲了一大堆,实现起来只是变了两行代码而已,但是带来的查找效率却不低。我们来看一下具体的测试:

 

测试200万次操作的效率:

这是没有路径压缩时的效率:


 

加入路径压缩操作时的效率:

 

我们可以发现,效率提高了很多。

如需访问此版本的全部源代码,请点击此处

 

二.基于递归的路径压缩操作


上面我们讨论了基于循环方法的路径压缩,每次寻根遍历元素的同时对元素进行压缩,减少层数,然而,我们知道,上面我们讲的路径压缩的方法并没有把层数给压缩到最小,最优的压缩方法应该如下图所示:

这样的压缩操作使得集合树只有两层。所有的节点都指向根节点,这种情况下,我们搜索任何节点的根节点都最多只需要一步就能够完成,因此,我们能不能设计另外一个算法,把集合树压缩成上面的两层形状呢?答案是可以的。现在让我们具体的来讨论如何实现:

因为find操作返回的是集合的根节点,因此我们只需要集合中所有的非根节点的父亲指针都指向这个根节点就好了,我们完全可以用递归的方法去实现:(参考我前面讲的递归的思路来设计)

1.首先设终止条件及其操作,如果访问的节点是根节点,则直接返回就好了

 

if(p==parent[p]) {//此时p为根节点
    return p;//返回该结合的根节点
}

2.设置非终止条件时应该执行的传递操作,find(parent[p])使得递归往根节点深入下去。

 

 

else{//此时遍历的不为根节点
    parent[p]=find(parent[p]);//通过递归条用find(parent[p])来向上访问元素,并且把找到的根元素赋值给节点的父亲指针
    return parent[p];//返回此时访问的节点的父亲指针指向的节点,也就是这个集合的根节点
}

3.find(parent[p])返回来的是集合中的根元素,我们把现在正在访问的这个元素的父亲指针指向上一步返回回来的find(parent[p]),此时的parent[p]就不仅仅是现在访问的元素的父亲节点了,还是集合树中的根节点,我们在把parent[p]返回给再上面的一层,让其元素的父亲指针也指向parent(p)(即根节点),这样一层接一层,最后这条递归传递路线上所有的元素的父亲指针都会指向那个根节点了,也就完成了我们的设计。

 

现在,我们来看一下这个算法的效率如何吧。同样是进行200万次的操作:

这样的路径压缩效率同样也是很高的,虽然递归的使用会增加额外的开销,但是我们认为是值得的。

在并查集的所有操作中,时间复杂度是近乎于O(1)的,在每一次经过路径压缩后,每一个节点就离根节点非常的近了,此时再执行查询操作就非常的快了。

如需访问所有的源代码(最终版本),请点击此处

基础不牢?新手不友好?无人带路?关注《扬俊的小屋》公众号吧!


 

 

  • 48
    点赞
  • 138
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值