并查集(Union-Find) 文档


起因

最近做的需求中遇到了对各种公告依照关联关系去重复的问题,偶然间学习到了并查集。

一、什么是并查集

并查集(Disjoint Set Union,简称 DSU 或 Union-Find)是一种 树形结构的数据结构 ,用于解决 动态连通性问题

它的核心作用是高效地回答两个问题:

  1. Find(查找) :判断某个元素属于哪个集合(找到它的"代表"或"根")。
  2. Union(合并) :把两个集合合并为一个。

比如:有一群人,他们之间有"朋友关系",每次你可以告诉系统"两个人是朋友"。

系统需要能快速回答:"这两个人是不是朋友?" 或 "他们属于同一个朋友圈吗?"

并查集就是为这种"分组 + 判断是否同组"的问题而生的。


二、原理与优化策略

1. 基本原理

每个元素都属于一个集合,使用一个数组 parent[] 来存储:
parent[x] 表示 x 的父节点

  • 如果 parent[x] = x,说明它是一个集合的代表(根节点)。
  • 查找根节点时,不断沿着父节点向上找,直到找到自己是自己的父节点。

2. 路径压缩(Path Compression)

路径压缩是优化 find 的手段,使集合结构更扁平。

原理:

在查找根节点时,将路径上所有节点的父节点都直接指向根节点。

这样后续查询同一个集合的元素时,查找速度更快。

例子:

初始时:

复制代码
1 → 2 → 3 → 4

查找 find(1) 时路径压缩后:

复制代码
1 → 4
2 → 4
3 → 4
4 → 4

结构变得更"平",后续查找会更快。

3. 按秩合并(Union by Rank / Size)

当两个集合合并时,我们希望保持树的深度尽可能小。

做法:

  • 为每个集合维护一个"rank"或"size"。
  • 在合并时,让"rank"小的树挂到"rank"大的树下面。

例如:

css 复制代码
集合A 深度=2,集合B 深度=1
→ 把B挂在A下,而不是反过来。

这样可以避免树变得太"高"。


三、Ruby 实现

下面给出两种实现方式:

1. 递归实现(路径压缩版)

ruby 复制代码
class UnionFind
  attr_reader :parent, :rank

  def initialize(n)
    @parent = Array.new(n) { |i| i }
    @rank = Array.new(n, 0)
  end

  # 查找根节点(递归 + 路径压缩)
  def find(x)
    return x if @parent[x] == x
    @parent[x] = find(@parent[x])
  end

  # 按秩合并
  def union(x, y)
    root_x = find(x)
    root_y = find(y)
    return if root_x == root_y

    if @rank[root_x] < @rank[root_y]
      @parent[root_x] = root_y
    elsif @rank[root_x] > @rank[root_y]
      @parent[root_y] = root_x
    else
      @parent[root_y] = root_x
      @rank[root_x] += 1
    end
  end
end
优点:
  • 实现简洁,逻辑清晰。
  • Ruby 的递归函数写法优雅。
缺点:
  • 当数据规模较大(如 10⁵ 级别)时,递归层数可能导致栈溢出。
  • 性能略低于迭代实现(函数调用有开销)。

2. 非递归实现(路径压缩版)

ruby 复制代码
class UnionFindIter
  attr_reader :parent, :rank

  def initialize(n)
    @parent = Array.new(n) { |i| i }
    @rank = Array.new(n, 0)
  end

  # 查找根节点(非递归 + 路径压缩)
  def find(x)
    root = x
    # 找到根节点
    root = @parent[root] while @parent[root] != root

    # 路径压缩
    while x != root
      parent = @parent[x]
      @parent[x] = root
      x = parent
    end
    root
  end

  # 按秩合并
  def union(x, y)
    root_x = find(x)
    root_y = find(y)
    return if root_x == root_y

    if @rank[root_x] < @rank[root_y]
      @parent[root_x] = root_y
    elsif @rank[root_x] > @rank[root_y]
      @parent[root_y] = root_x
    else
      @parent[root_y] = root_x
      @rank[root_x] += 1
    end
  end
end
优点:
  • 没有递归,适合大规模数据。
  • 执行速度更稳定。
缺点:
  • 实现稍复杂,代码略长。

性能比较总结:

实现方式 路径压缩 按秩合并 是否递归 时间复杂度 空间开销 适用场景
递归 近似 O(α(n)) 小规模或教学
非递归 近似 O(α(n)) 稍高 大规模、高性能

注:α(n) 是阿克曼函数的反函数,增长极慢,可视为常数时间。


四、使用场景与实战案例

1. 常见使用场景

  • 网络连通性判断:判断两台计算机是否在同一个局域网。
  • 社交网络分组:判断两人是否在同一个朋友圈。
  • 图的连通块统计:计算图中有多少个独立的连通分量。
  • 最小生成树(Kruskal算法) :判断边的两端是否属于同一集合。
  • 图像分割 / 聚类分析:把像素或点按相似度聚为一类。

2. 实战案例:朋友圈分组统计

假设我们有 5 个人(编号 0~4),朋友关系如下:

scss 复制代码
(0, 1), (1, 2), (3, 4)

我们希望知道一共有几个独立的朋友圈。

ini 复制代码
uf = UnionFindIter.new(5)
relations = [[0,1],[1,2],[3,4]]
relations.each { |a,b| uf.union(a,b) }

# 统计根节点数量
roots = uf.parent.map { |x| uf.find(x) }.uniq
puts "朋友圈数量:#{roots.size}"

输出:

复制代码
朋友圈数量:2

说明存在两个朋友圈:

  • {0,1,2}
  • {3,4}

五、总结

  • 并查集是一种 高效维护动态连通性 的结构。

  • 优化手段包括:

    • 路径压缩:让树更平。
    • 按秩合并:让树不长。
  • 在 Ruby 中既可以用递归,也可以用非递归实现。

  • 常用于 图算法、社交关系、网络连通、聚类分析 等场景。


相关推荐
YDS8293 小时前
苍穹外卖 —— 文件上传和菜品的CRUD
java·spring boot·后端
bcbnb3 小时前
Fiddler抓包实战教程 从安装配置到代理设置,详解Fiddler使用方法与调试技巧(HTTPHTTPS全面指南)
后端
颜颜yan_3 小时前
Rust impl块的组织方式:从基础到实践的深度探索
开发语言·后端·rust
xiguolangzi3 小时前
mysql迁移PG库 主键、唯一处理、批量修改
java·后端
Cache技术分享3 小时前
224. Java 集合 - 使用 Collection 接口存储元素
前端·后端
小刘大王3 小时前
伴生类和单例对象
前端·后端
Lear3 小时前
SpringBoot 如何删除清理垃圾文件?
后端
BingoGo3 小时前
Laravel 新项目避坑指南10 大基础设置让代码半年不崩
后端·php
JMzz4 小时前
Rust 中的数据结构选择与性能影响:从算法复杂度到硬件特性 [特殊字符]
开发语言·数据结构·后端·算法·性能优化·rust