小知识:并查集的实现

377 阅读2分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

并查集一般用在对集合模拟合并和查找操作。但是貌似并没有一个比较官方的实现。

我们手动实现的话,就先最简单的效果开始,假设有5个元素,每个数属于一个集合。我们为它储存一个状态,假设单独作为一个集合是用 -1来记录。

五个顶点,分别是 0,1,2,3,4 .

a,b,c,d,e = 0,1,2,3,4 # 五个顶点
parent = [-1]*5 # 储存状态,叫parent是因为后面它会像一棵树

接下来考虑现在这种情况的合并的操作,如果要合并ac这两个集合。具体的操作是把a或者c中的一个当作一个树形结构的父节点,另一个则是子节点。假设a做父节点。

比如像下面这样

parent[c] = a

image.png

这样之后c保存的状态就不是-1了,找c所在的集合就得通过它的父节点 来找,如果是小于0(后文还会解释,现在是只考虑-1,它也属于小于0的情况)表示找到了这个集合最开始的节点,这一个集合都用这一个顶点来标识 也就分辨得出是同一个集合 。现在只有一层,但是即使多几层也会发现是一样的过程。

所以查找操作就可以是下面这样

def find(parent: List[int],e: int)->int:
    if parent[e] < 0: 
        return e
    return find(parent,parent[e])

当对两个元素进行查找的时候,如果两个元素并不属于同一个集合,可以做一个简单的合并操作,把一个集合的根顶点放到另一个集合的根顶点下,当作子节点。因为find函数查找到的本就是根节点,直接对这个值做操作就行了。

像下面这样

def union(parent: List[int],x: int,y: int):
    x = find(parent,x)
    y = find(parent,y)
    parent[x] = y

简单的合并操作已经完成了。

但其实这是有问题的,如果每次y都是只有一层的树,而x则是多层,则每次表示集合的树就会加一层,最坏的情况下,会达到和parent数组长度一致的深度。

image.png 需要避免这种情况,这时候就用上了我们先前说的那个小于0。

在最开始的时候,我们把它初始化为了-1,可以做一个这样的解读,-1表示树的深度的负数。

然后在合并的时候我们总是做一个这样的操作,当两个树一样高的时候,就随意连接,然后把作为父节点的树的深度加一层。而一长一短的情况,则可以把短的树作为长的树的子节点,这样的情况下,树的深度是不会增加的。

也就是像下面这样

def union(parent: List[int],x: int,y: int):
    x = find(parent,x)
    y = find(parent,y)
    if x == y:
        parent[x] = y
        parent[y] -=1
    elif x > y: # -x < -y,x 接到 y
        parent[x] = y
    else:
        parent[y] = x

到这里并查集的实现已经完成了。时间复杂度为log(n)级别。

如果需要它更好使用,可以把它封装成类。在这种实现下,可能这个parent数组有些让人认为不合适,所以也有有另一种实现:它把这里parent数组标识深度的功能划分到另一个数组,然后这个parent数组只放父节点的位置。对于它的集合的根节点则使用自身来表示,如果不与自身相等则需要查父节点。

二者的实现从功能上来讲完全一样,不过另一种写法的规范性更强一些。除此之外,如果parent不使用数组而使用哈希表实现,甚至可以使用命名节点,以得到更高的通用性。