并查集的路径压缩:并查集优化,让查找更快

在数据结构的世界里,有一种叫做并查集(Union-Find)的工具,它专门用来解决一类“集合合并”和“元素归属”的问题。比如,我们可能想知道“小明和小红是不是在同一个朋友圈”,或者“两个网络节点是否连通”,这些问题都可以用并查集来高效解决。

并查集的基本操作

并查集主要有两个核心操作:find(查找元素所在集合的根节点)和union(合并两个集合)。为了理解这两个操作,我们先假设有一个简单的场景:
我们有编号为1到5的5个元素,一开始每个元素都是自己的“集合”,就像每个人都属于自己的朋友圈。我们用一个数组parent来记录每个元素的“父节点”(即直接指向的上一级元素),初始时parent[i] = i(自己是自己的父节点)。

1. find操作:找“祖宗”

find(x)的目标是找到元素x所在集合的“根节点”(即这个集合的“祖宗”)。比如,如果我们合并了1和2,那么parent[2] = 1,此时find(2)的结果就是1(因为2的父节点是1,而1的父节点是自己,所以1是根)。

基础版find的逻辑很简单:

def find(x):
    while parent[x] != x:  # 如果x的父节点不是自己,就一直向上找
        x = parent[x]
    return x  # 找到根节点,返回

2. union操作:合并集合

union(x, y)的目标是把xy所在的两个集合合并成一个。比如,我们要合并1和2所在的集合,只需要让其中一个集合的根指向另一个集合的根。
基础版union的逻辑是:

def union(x, y):
    root_x = find(x)
    root_y = find(y)
    if root_x != root_y:
        parent[root_y] = root_x  # 让y的根指向x的根

基础版并查集的问题:查找太慢了!

基础版并查集虽然能用,但在某些场景下效率很低。比如,当我们多次合并后,集合可能形成一个“长链”结构:
假设我们依次合并了1-2、2-3、3-4、4-5,此时parent数组的状态会是:
parent[1]=1parent[2]=1parent[3]=2parent[4]=3parent[5]=4

这时,如果我们要find(5),需要依次经过5→4→3→2→1,才能找到根节点1。如果这样的“长链”很长(比如合并了1000个元素),每次find都要走1000步,效率就会变得很低。

路径压缩:让查找“一步到位”

为了解决长链问题,路径压缩(Path Compression)应运而生。它的核心思想是:在每次find操作中,让路径上的每个节点直接指向根节点,这样下次查找时路径会被“扁平化”,几乎“一步到位”。

路径压缩的原理

想象你在爬山,本来需要经过很多台阶才能到山顶(根节点)。路径压缩就像在你爬的过程中,每次遇到台阶就直接把你拉到山顶,下次再爬时,你直接从山顶出发,不需要再走台阶了。

具体实现时,我们可以在find函数中对路径上的每个节点进行“压缩”:当递归找到根节点后,把路径上所有节点的父节点都直接设为根。

带路径压缩的find实现

以下是带路径压缩的find函数(递归版):

def find(x):
    if parent[x] != x:  # 如果x不是根节点
        # 递归找到根节点,并把父节点直接设为根
        parent[x] = find(parent[x])
    return parent[x]

举个例子
假设合并后形成的长链是5→4→3→2→1(即parent[5]=4parent[4]=3parent[3]=2parent[2]=1parent[1]=1)。

  • 第一次find(5)
    递归执行find(4)find(3)find(2)find(1)(此时parent[1]=1,返回1)。
    然后parent[2]被设为1,parent[3]被设为1,parent[4]被设为1,parent[5]被设为1。
    此时parent数组变为:parent[5]=1parent[4]=1parent[3]=1parent[2]=1parent[1]=1

  • 第二次find(5)
    直接parent[5] = 1,返回1,路径长度从5步变成1步!

迭代版路径压缩(更直观)

如果觉得递归版难理解,也可以用迭代方式实现路径压缩:

def find(x):
    # 第一步:找到根节点
    root = x
    while parent[root] != root:
        root = parent[root]
    # 第二步:把路径上所有节点的父节点设为根(路径压缩)
    while parent[x] != root:
        next_parent = parent[x]  # 记录当前父节点
        parent[x] = root         # 直接指向根
        x = next_parent          # 继续处理下一个节点
    return root

这个迭代版本先找到根节点,再从起点开始,把每个节点的父节点直接设为根,确保路径最短。

路径压缩的效果:几乎O(1)的查找效率

路径压缩让find操作的时间复杂度从基础版的O(h)(h为树的高度)优化到接近O(1)。即使是对100万个元素的集合,经过路径压缩后,查找一次的时间也几乎可以忽略不计。

这是因为每次路径压缩都会让树的结构变得更扁平,最终每个节点都会直接指向根,形成一棵“矮胖”的树。

总结

路径压缩是并查集的“加速魔法”,它通过在查找过程中让所有节点直接指向根节点,把原本可能很长的路径压缩成“一步到位”。结合并查集的其他优化(如按秩合并),它能在几乎常数时间内完成合并和查找操作,成为解决“集合问题”的核心技术。

下次遇到需要快速判断元素归属的问题时,记得用并查集+路径压缩,让你的代码飞起来吧!

小夜