并查集(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 中既可以用递归,也可以用非递归实现。

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


相关推荐
章豪Mrrey nical5 小时前
前后端分离工作详解Detailed Explanation of Frontend-Backend Separation Work
后端·前端框架·状态模式
派大鑫wink7 小时前
【JAVA学习日志】SpringBoot 参数配置:从基础到实战,解锁灵活配置新姿势
java·spring boot·后端
程序员爱钓鱼7 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
xUxIAOrUIII7 小时前
【Spring Boot】控制器Controller方法
java·spring boot·后端
Dolphin_Home7 小时前
从理论到实战:图结构在仓库关联业务中的落地(小白→中级,附完整代码)
java·spring boot·后端·spring cloud·database·广度优先·图搜索算法
zfj3217 小时前
go为什么设计成源码依赖,而不是二进制依赖
开发语言·后端·golang
weixin_462446237 小时前
使用 Go 实现 SSE 流式推送 + 打字机效果(模拟 Coze Chat)
开发语言·后端·golang
JIngJaneIL8 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小信啊啊8 小时前
Go语言切片slice
开发语言·后端·golang
Victor35610 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端