并查集路径压缩(千字长文)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 82w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2900+ 小伙伴加入学习 ,欢迎点击围观

并查集基础:从集合到树形结构

并查集(Disjoint Set Union, DSU)是一种用于管理动态集合的数据结构,核心功能是支持两种操作:

  1. 查找(Find):判断两个元素是否属于同一集合。
  2. 合并(Union):将两个不同集合合并为一个集合。

想象一个家族树:每个节点代表一个人,而并查集的作用就是快速判断两个人是否属于同一家族,或者将两个家族合并。这种结构在社交网络分析、图的连通性问题中广泛应用。

并查集的基础实现通常使用树形结构,每个节点存储其父节点的索引。例如,用数组 parent 表示,其中 parent[i] 表示节点 i 的父节点。初始时,每个元素的父节点指向自身(parent[i] = i)。

基础实现代码示例

class UnionFind:  
    def __init__(self, size):  
        self.parent = list(range(size))  # 初始时每个元素是独立的集合  

    def find(self, x):  
        while self.parent[x] != x:  
            x = self.parent[x]  
        return x  

    def union(self, x, y):  
        root_x = self.find(x)  
        root_y = self.find(y)  
        if root_x != root_y:  
            self.parent[root_y] = root_x  

路径压缩:让树更扁平化

在基础并查集中,频繁的 find 操作可能导致树的高度变得很高,例如线性链表结构。假设查找一个位于链表末端的节点,时间复杂度会达到 O(n),这显然不够高效。

路径压缩(Path Compression) 是一种优化策略,它通过将查找路径上的所有节点直接指向根节点,从而缩短树的高度。这一过程类似于“修剪树枝”,让树的结构更扁平,接近森林中“矮灌木丛”的形态。

路径压缩的实现原理

find 方法中,路径压缩的核心逻辑是:

  1. 查找节点 x 的根节点。
  2. 在查找过程中,将路径上的所有节点直接指向根节点。

例如,假设路径为 x → A → B → root,路径压缩后,xAB 的父节点都会被更新为 root

递归实现路径压缩

def find(self, x):  
    if self.parent[x] != x:  
        self.parent[x] = self.find(self.parent[x])  # 路径压缩  
    return self.parent[x]  

这段代码通过递归,将路径上的每个节点的父指针直接指向根节点,从而实现扁平化。

迭代实现路径压缩

def find(self, x):  
    original_x = x  
    # 查找根节点  
    while self.parent[x] != x:  
        x = self.parent[x]  
    # 压缩路径  
    while self.parent[original_x] != x:  
        next_parent = self.parent[original_x]  
        self.parent[original_x] = x  
        original_x = next_parent  
    return x  

迭代版本通过两次循环,先找到根节点,再回溯路径修改父指针。

路径压缩的优势与性能分析

时间复杂度优化

引入路径压缩后,并查集的 findunion 操作的时间复杂度从 O(n) 降低到 近似 O(1)。具体而言,路径压缩结合按秩合并(Rank)的策略,可以将时间复杂度优化到 O(α(n)),其中 α 是阿克曼函数的反函数,增长极慢(在 n 实际应用范围内,α(n) ≤ 5)。

实际效果对比

假设有一个包含 1000 个节点的线性链表结构:

  • 无路径压缩:查找最后一个节点需要遍历 1000 次。
  • 有路径压缩:第一次查找后,所有节点的父指针直接指向根,后续查找只需 1 次操作。

这种优化对大规模数据的处理尤为重要。

典型应用场景与案例分析

案例 1:迷宫路径连通性检测

假设需要判断迷宫中两个点是否连通,可以将迷宫的每个格子视为节点,用并查集管理连通区域。路径压缩能显著加速频繁的连通性查询。

class MazeSolver:  
    def __init__(self, rows, cols):  
        self.uf = UnionFind(rows * cols)  
        # 假设已知相邻格子的连通性,合并对应的集合  
        ...  

    def is_connected(self, x, y):  
        return self.uf.find(x) == self.uf.find(y)  

案例 2:社交网络的朋友分组

在社交网络中,用户分组问题可转化为并查集的合并操作。路径压缩能快速响应用户频繁的分组查询和合并操作。

性能对比实验

假设测试数据规模为 1,000,000 个节点,进行 100,000 次随机合并和查找操作:
| 策略 | 平均查找时间 | 平均树高度 |
|--------------------|-------------|-----------|
| 无路径压缩 | 0.5秒 | ~500 |
| 仅路径压缩 | 0.02秒 | ~2 |
| 路径压缩 + 按秩合并 | 0.01秒 | ~1 |

实现细节与注意事项

1. 路径压缩与按秩合并的结合

路径压缩通常与 按秩合并(Union by Rank) 结合使用,进一步优化树的平衡性。按秩合并的规则是:

  • 合并时,将秩(树的高度)较小的树挂到秩较大的树下。
  • 若两棵树的秩相同,则任选一棵作为根,并将另一棵的秩加 1。
class UnionFindWithRank:  
    def __init__(self, size):  
        self.parent = list(range(size))  
        self.rank = [1] * size  # 初始秩为1  

    def find(self, x):  
        if self.parent[x] != x:  
            self.parent[x] = self.find(self.parent[x])  
        return self.parent[x]  

    def union(self, x, y):  
        root_x = self.find(x)  
        root_y = self.find(y)  
        if root_x == root_y:  
            return  
        if self.rank[root_x] < self.rank[root_y]:  
            self.parent[root_x] = root_y  
        else:  
            self.parent[root_y] = root_x  
            if self.rank[root_x] == self.rank[root_y]:  
                self.rank[root_x] += 1  

2. 递归与迭代的权衡

  • 递归实现路径压缩:代码简洁,但可能因栈溢出问题不适用于极深的树。
  • 迭代实现路径压缩:避免栈溢出,但代码稍复杂。

3. 空间复杂度

并查集的空间复杂度始终为 O(n),因为仅需存储父节点和秩数组。

常见问题与解答

Q1:路径压缩是否会影响其他操作?

路径压缩仅在 find 操作中执行,不会干扰 union 的合并逻辑,因此是安全的。

Q2:如何验证路径压缩的效果?

可通过统计 find 操作中遍历的节点数量。例如,无路径压缩时,路径长度可能随操作次数增长,而路径压缩后路径长度迅速收敛到 1。

Q3:为什么路径压缩不会破坏按秩合并的策略?

路径压缩仅调整父指针,不影响秩数组的记录。因此,按秩合并的规则仍能保证树的平衡性。

结论:路径压缩的价值与应用场景

路径压缩是并查集优化的核心技巧,它通过扁平化树的结构,将查找操作的时间复杂度降低到几乎常数级别。对于需要频繁处理集合合并与查询的场景(如社交网络分析、图的连通性问题、动态分组管理等),路径压缩能显著提升算法的效率。

掌握这一技巧后,开发者可以更自信地应对复杂的数据结构问题。例如,在 LeetCode 的“岛屿数量”或“朋友圈”等经典题中,路径压缩能帮助写出更高效的解决方案。

总之,并查集路径压缩不仅是算法优化的典范,更是理解数据结构动态调整逻辑的重要案例。通过结合按秩合并等策略,它为处理大规模数据提供了坚实的理论基础和实践指导。

最新发布