(2010-03-06)
1、定义
并查集(union-find sets)是一种简单的用途广泛的集合。并查集是若干个不相交集合(Disjoint Sets),能够实现较快的集合合并和判断元素所在集合的操作,应用很多。实际中通常采用树形结构来表示并查集:每棵树表示一个集合,不相交的集合则构成森林。一棵树的每个节点包含如下信息:父节点指针p[x],树的深度rank[x]。
2、操作
2.1 建立一个集合
初始时,为每个元素x都建立一个集合:Make-Set(x),集合的元素只有它自己,伪代码如下:
Make-Set(x)
p[x]←x
rank[x]←0
2.2 查找元素所在集合
由于我们用一棵树来表示集合,则为操作方便,每个集合有一个代表元素,我们将之定义为该树的根,则查找元素x所在集合的操作Find-Set(x)伪代码如下:
Find-Set(x)
if x=p[x]
then return x
else return Find-Set(p[x])
2.3 合并不相交集合
合并元素a所在集合和元素b所在集合是将a所在树的根节点的父节点改为b所在树的根节点。这个操作Unin(a,b)只需O(1)的时间。
Union(a,b)
Link(Find-Set(a),Find-Set(b)) /*Link为辅助函数,作用如下:*/
Link(a,b)
p[a]←b
由此可见Union的速度取决于Find-Set的速度。
3、优化
上述Find-Set(x)操作的时间复杂度与树的深度成线性关系,因此其平均时间复杂度为O(logN),但在最坏情况下(树退化成链表),时间复杂度为O(N)。所以有必要对算法进行优化。
3.1 启发式合并
在Link函数中,我们将深度较小的树指到深度较大的树的根上。这样可以防止树的退化,最坏情况不会出现。可以证明:这样优化后树的深度为O(log N),所以Find-Set(x)的时间复杂度为O(log N)。改进后的Link函数伪代码如下所示:
Link(a,b)
if rank[a]>rank[b]
then p[b]←a
else p[a]←b
if rank[a]=rank[b]
then rank[b]←rank[b]+1
3.2 路径压缩
为了使得Find-Set(x)的效率更高,我们还要进行路径压缩,即在找到x的最远祖先(集合代表根)时顺便把查找路径上的子孙的父节点直接改为该根,这样以后Find-Set的时间复杂度可以认为是常数的。改进后的Find-Set伪代码如下:
Find-Set(x)
if p[x]≠x
then p[x]←Find-Set(p[x])
return p[x]
递归的程序有许多栈的操作,改成非递归会更快些:
Find-Set(x)
r←x
while p[r]≠ r
do r←p[r]
while r≠x
do q←p[x]
p[x]←r
x←q
return r
3.3 时间复杂度
经过启发式合并和路径压缩之后的并查集进行n次查找的时间复杂度是O(nα(m,n))(执行n-1次合并和m>=n次查找)。其中α(m,n)是一个增长极其缓慢的函数,是Ackerman函数的某个反函数,它可以看作是小于5的。所以可以认为并查集的时间复杂度几乎是线性的。
【附】
Ackerman函数 一种双递归函数,满足以下定义: