算法界女神之并查集详解

3,305 阅读7分钟

各位同学晚上好,我是leon,欢迎大家参与今天的算法课堂。在上次的调查中,没想到有那么多同学对并查集感兴趣,所以今天给大家来讲一讲并查集。


在说并查集之前,我们先讲一下合并-查找问题。


什么是合并查找问题呢?顾名思义,就是既有合并又有查找操作的问题。举个例子,有一群人,他们之间有若干好友关系。如果两个人有直接或者间接好友关系,那么我们就说他们在同一个朋友圈中,这里解释下,如果Alice是Bob好友的好友,或者好友的好友的好友等等,即通过若干好友可以认识,那么我们说Alice和Bob是间接好友。随着时间的变化,这群人中有可能会有新的朋友关系,这时候我们会对当中某些人是否在同一朋友圈进行询问。这就是一个典型的合并-查找操作问题,既包含了合并操作,又包含了查找操作。


这个问题很容易想到朴素算法,每个人用一个编号来表示他所在的朋友圈,如果有新认识的朋友,我们就合并朋友圈,即把两人的朋友圈中所有人编号标识成一样。



简单解释一下这个图,刚开始A和B是好友,用蓝色背景标记,C和D是好友,用绿色背景标记,如果询问两个人是否在同一个朋友圈,那么只要看一下他们标记是否相同。过了不久,A和D又成为了好友,我们把两个朋友圈中所有的人标记变成相同的颜色,这就完成了一次合并的操作。


我们来粗略的计算下复杂度,假设我们要合并Alice和Bob的朋友圈,需要找到所有和Bob在同一朋友圈里的人,并标记为Alice所在的朋友圈。代码如下

合并代码非常简单,那么可以看到一次合并的复杂度为O(n),一共有n个人,因此最多可能要合并n-1次,合并的复杂度为n^2,加上Q个朋友圈询问,那么总的复杂度就是O(n^2+Q)。当n很大的时候,算法是不可接受的。


这个时候我们就希望重新设计一种数据结构,能够高效的处理这三种操作,分别是

  • MAKE-SET(x),创建一个只有元素x的集合,且x不应出现在其他的集合中

  • UNION(x, y),将元素x所在集合Sx和元素y所在的集合Sy合并,这里我们假定Sx不等于Sy

  • FIND-SET(x),查找元素x所在集合的代表


现在我们再来看一下刚刚那道题,起初每个人都互相不认识,我们可以调用n次MAKE-SET来创建n个集合,当有两个人互相认识的时候,那么我们用UNION来合并两个集合。最后,如果询问x和y是否在同一个朋友圈的时候,我们调用两次FIND-SET来判断x和y所在集合的代表是否相同,从而判断他们是否在同一个集合内。


这个时候就该并查集出场了,它能够高效的处理前面提到的三种操作,用于维护一系列不相交集合动态操作的数据结构。每个不相交集合都有一个代表用于表示整个集合,并且这个代表也是集合内的成员。下面来说一下并查集是如何工作的。在并查集中,每个不相交的集合都用一颗有根树来表示,每个元素都是树上的一个节点,我们继续用刚刚的朋友圈问题来解释。


我们来解释下这个图,起初有5个人,编号为1-5,刚开始大家都互不认识,所以各自为一个节点的树。这时候如果1和2认识了,那么我们就把这两个节点所代表的树合并起来,由编号较小的1作为根。接着3和4又认识了,那么我们重复刚刚的过程,把3和4所代表树合并起来。现在1和4认识了,该怎么办呢,首先我们先找到1和4所在树的根,1的根就是1,而4的根就是3,这时候我们把这两颗树合并,并把1设置为3的父亲节点,这时候就完成了两颗树的合并。 在这系列操作中,初始化构建树就是MAKE-SET操作,树的合并就是UNION操作,而找根的过程就是FIND-SET操作。


下面讲一讲并查集的具体实现对于有根树或者说森林的表示,可以用一个数组parent来实现。parent[i]记录元素i的父节点的编号,如果节点i本身就是根节点,那么parent[i]就是-1。MAKE-SET操作就是将parent数组赋值为-1,代码比较简单就不贴了。UNION操作需要找到两个元素的根,并把其中一个元素的根节点设置为另一个元素的根节点,其实就是调用了两次FIND-SET,代码如下

再来说下FIND-SET这个操作的实现。首先有个很朴素的算法,对父节点递归调用FIND-SET,直到找到根为止。如果父节点是-1,那么返回当前节点,否则递归调用查询父亲的根节点。代码如下

细心的同学会发现,如果我们这棵树很深,那么每次调用FIND-SET可能会需要O(n)的时间,总的复杂度在最坏情况下就是O(nQ)了,那不是更差了吗?


这时候,我们就要引入路径压缩这个概念。什么是路径压缩呢?就是在递归找到根节点的时候,把当前节点到根节点间所有节点的父节点都设置为根节点。举个例子

我们来看下图,首先我们有个这样的一棵树,现在要找到元素9所在树的根节点,在找根节点的过程中使用路径压缩,也就是说9到根的路径上的节点9,6,3,1的父节点都设置成为根节点0,所以呢,在FIND-SET(9)之后,树的形态就变成了下面的样子

我们可以看到经过路径压缩的节点及其子树到根节点的深度减小了很多,所以在以后的操作中,查找根节点的速度会快很多,平摊下来每次FIND-SET的操作几乎是常数级别的。代码也相当简单,就只多了一句话

刚刚在合并的过程中,并没有提到说到如何选取根,一般情况下两个根节点随意选取一个,如果需要更优的算法,可以按秩合并。除此以外,并查集还能处理带权的情况,因为时间的关系就不在线上分享了。


最后总结一下,并查集是一种支持合并集合和查找集合的一种数据结构,能用均摊线性的复杂度执行各种操作,在kruskal算法、求联通分支数等算法中起到关键的作用。


好了,今天的分享就到这里,谢谢大家的收听。有任何问题,可以在评论区留言。


往期回顾




我们是InitOffer,致力于为IT新人提供求职干货、学习资料,更有谷歌、微软、BAT大牛助阵,提供在线简历修改,工作经验分享,模拟面试训练等超值内容。