【UCB CS 61B SP24】Lecture 14 - Data Structures 1: Disjoint Sets学习笔记

news/2025/2/26 8:22:05

本文内容为数据结构并查集(DSU)的介绍与实现,详细讲解了并查集这一数据结构所能实现的各种操作,以及如何通过路径压缩与按秩合并大幅优化并查集的效率。

1. 并查集

1.1 介绍及其基础操作

并查集(Disjoint Set Union,DSU,也称为不相交集合)是一种用于管理一组不相交集合的数据结构。它支持以下两种主要操作:

  • 查找(Find):确定某个元素属于哪个集合。
  • 合并(Union):将两个集合合并为一个集合。

并查集在解决动态连通性问题(如网络连接、图的连通分量、最小生成树等)时非常高效。

并查集中的每个集合有一个代表元(也成为根节点),用于标识该集合。并查集通常使用数组来表示,每个元素存储其父节点的引用,根节点的父节点指向自己。

(1)初始化

假设有6个元素:{0, 1, 2, 3, 4, 5},初始时每个元素都是一个独立的集合,即每个元素的父节点是它自己,我们用 pre[i] 表示第 i i i 个节点的父节点,那么初始化时 pre 的内容如下:

java">pre = [0, 1, 2, 3, 4, 5]

(2)Find

查找操作的实现思路是从当前元素开始,沿着父节点逐级向上查找,直到找到根节点(每个集合中父节点指向自己的节点就是这个集合的根节点),该算法 find(x) 返回的即为节点 x x x 所在集合的根节点序号。

例如当前的 pre 数组为 pre = [0, 1, 1, 2, 5, 3],那么查找5号节点所属集合的算法流程如下:

  • 执行 find(5),由于 pre[5] = 3,因此5号节点不是根节点,继续查找3号节点也就是其父节点所在的集合;
  • 执行 find(3),由于 pre[3] = 2,因此3号节点不是根节点,继续查找2号节点所在的集合;
  • 执行 find(2),由于 pre[2] = 1,因此2号节点不是根节点,继续查找1号节点所在的集合;
  • 执行 find(1),由于 pre[1] = 1,因此1号节点是整个集合的根节点,最后查找算法返回1。

我们用递归的思想可以很容易实现查找操作(先用 C++ 演示):

int find(int k) {
    if (pre[k] == k) return k;
    return find(pre[k]);
}

有了 Find 操作后我们想判断两个节点 x , y x,y x,y 是否连通(在同一个集合中)就很简单了,即判断 find(x) 是否等于 find(y)

(3)Union

合并操作的实现思路是找到两个元素的根节点,然后将其中一个根节点的父节点指向另一个根节点,很容易实现:

void union(int x, int y) {
    int px = find(x), py = find(y);
    if (px != py) pre[px] = py;
}

1.2 路径压缩

为了提高并查集的效率,通常会使用两种优化技术分别为路径压缩(Path Compression)与按秩合并(Union by Rank)。

路径压缩的实现思路是在 Find 操作中,将查找路径上的所有节点直接指向最后的根节点,从而减少后续查找的时间,即在递归查找的同时递归地将每个节点的父节点更新为根节点:

int find(int k) {
    if (pre[k] == k) return k;
    return pre[k] = find(pre[k]);
}

假设当前父节点数组为 pre = [0, 1, 1, 2, 5, 3],那么在执行完一次 find(5) 后该数组的内容就更新为 pre = [0, 1, 1, 1, 5, 1],这样之后如果还要继续查询3号节点或5号节点所在的集合时就不用在递归查找好几次了。

结合图片来理解一下,在下面这样的并查集结构下我们想查看10与15是否相连,那么我们便执行了 find(10)find(15),执行 Find 操作时沿着蓝色的路径一路向根节点查询:

在这里插入图片描述

使用路径压缩后我们将查询时一路上走过的节点都更新其父节点为根节点,那么更新后的并查集就变成了下面这样,能够有效地优化整棵树的深度:

在这里插入图片描述

在没有路径压缩的情况下,Find 操作的时间复杂度取决于树的深度。最坏情况下,如果所有元素都依次合并成一条链(即树的高度为 n n n,其中 n n n 是元素的数量),则每次查找最末尾的那个元素都需要遍历整个链,时间复杂度为 O ( n ) O(n) O(n),若有 m m m 次查找,则时间复杂度为 O ( n m ) O(nm) O(nm)

路径压缩显著减少了查询后树的高度,未来的查询会变得非常快,在进行多次操作后,路径压缩导致树的高度趋向于非常小(通常为 O ( l o g n ) O(log n) O(logn)),且查找操作的时间复杂度会接近 O ( α ( n ) ) O(\alpha (n)) O(α(n)),其中 α ( n ) \alpha (n) α(n) 是阿克曼函数的反函数,它是一个非常缓慢增长的函数,可以认为是常数时间复杂度,即 O ( α ( n ) ) ≈ O ( 1 ) O(\alpha (n)) \approx O(1) O(α(n))O(1)

1.3 按秩合并

在这里插入图片描述

按秩合并的实现思路是在 Union 操作中,将较小的树合并到较大的树中,从而避免树的高度过高,我们可以用 rank[] 数组来记录每个集合的秩(或高度),用于按秩合并操作,每个集合的秩初始化为1,在 Union 操作中如果两棵树的秩相同,则合并后将根节点的秩加一:

void connect(int x, int y) {
    int px = find(x), py = find(y);
    if (px != py) {
        if (rk[px] < rk[py]) pre[px] = py;
        else if (rk[px] > rk[py]) pre[py] = px;
        else pre[px] = py, rk[py]++;
    }
}

由于在 C++ 中 unionrank 已经是关键字了,因此此处我们将其分别改为 connectrk

2. Java实现DisjointSetUnion

经过前面的讲解,现在使用 Java 自己实现一个完整的并查集 DisjointSetUnion 就很简单了,只需要将所有方法组合在一起就是完整的并查集数据结构

java">package CS61B.Lecture14;

public class DisjointSetUnion {
    private int[] parent;
    private int[] rank;

    /**
     * 构造函数:初始化并查集
     * @param size 元素数量
     */
    public DisjointSetUnion(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i;
            rank[i] = 1;
        }
    }

    /**
     * 查找操作(带路径压缩)
     * @param x 要查找的元素
     * @return 元素 x 的根节点(集合代表)
     */
    public int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }

    /**
     * 合并操作(带按秩合并)
     * @param x 元素 x
     * @param y 元素 y
     */
    public void union(int x, int y) {
        int px = find(x), py = find(y);
        if (px != py) {
            if (rank[px] < rank[py]) parent[px] = parent[py];
            else if (rank[px] > rank[py]) parent[py] = parent[px];
            else {
                parent[px] = py;
                rank[py]++;
            }
        }
    }

    /**
     * 检查两个元素是否属于同一集合
     * @param x 元素 x
     * @param y 元素 y
     * @return true 如果属于同一集合,否则 false
     */
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }

    public static void main(String[] args) {
        DisjointSetUnion dsu = new DisjointSetUnion(5);
        dsu.union(0, 2);
        dsu.union(2, 3);
        dsu.union(1, 4);  // 此时并查集为:[{0, 2, 3}, {1, 4}]
        System.out.println(dsu.isConnected(0, 3));  // true
        System.out.println(dsu.isConnected(3, 4));  // false
        System.out.println(dsu.isConnected(1, 4));  // true
    }
}

这次我们将 Find 操作的代码用另外一种等价的形式写一遍,只是这种写法可能有人容易踩坑,就是最后返回的时候写成 return x; 这是不对的,因为只有对于根节点来说是满足 parent[x] == x 的,而且其 parent 数组中的值不会做更新,在递归回溯的时候所有子节点的 parent 会被更新,更新为从根节点逐级回溯来的值,因此最后需要返回根节点的值 parent[x],而不是返回当前节点 x


http://www.niftyadmin.cn/n/5868375.html

相关文章

冯诺依曼体系结构 ──── linux第8课

目录 冯诺依曼体系结构 关于冯诺依曼&#xff0c;必须强调几点&#xff1a; 冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系 输入单元&#xff1a;包括键盘, 鼠标&#xff0c;网卡,扫…

C#语音识别与播报开发指南

C# 语音识别离线开发推荐库 以下是一些适用于 C# 离线语音识别的库&#xff0c;支持本地处理&#xff0c;无需网络连接&#xff1a; 1. System.Speech.Recognition (Windows) 简介&#xff1a;.NET Framework 自带的库&#xff0c;适合简单的离线命令词识别。适用场景&#x…

Python--内置函数与推导式(上)

1. 匿名函数 Lambda表达式基础 语法&#xff1a;lambda 参数: 表达式​ 特点&#xff1a; 没有函数名&#xff0c;适合简单逻辑函数体只能是单行表达式自动返回表达式结果 # 示例1&#xff1a;加法 add lambda a, b: a b print(add(3, 5)) # 输出 8# 示例2&#xff1a;字…

selenium如何实现,开启浏览器的开发者工具模式,并且开启 toggle移动设备模拟模式

核心实现代码 pythonCopy Code from selenium import webdriver from selenium.webdriver.chrome.options import Options def enable_devtools_with_toggle(): options Options() # 强制开启开发者工具 options.add_argument("--auto-open-devtools-for-tabs&quo…

【图像处理 --- Sobel 边缘检测的详解】

Sobel 边缘检测的详解 目录 Sobel 边缘检测的详解1. 梯度计算2. 梯度大小3. 梯度方向4. 非极大值抑制5. 双阈值处理6. 在 MATLAB 中实现 Sobel 边缘检测7.运行结果展示8.关键参数解释9.实验与验证 Sobel 边缘检测是一种经典的图像处理算法&#xff0c;用于检测图像中的边缘。它…

《Linux命令行和shell脚本编程大全》第二章阅读笔记

二.走进shell 1.进入命令行 在图形化桌面出现之前&#xff0c;和 Unix 系统交互的唯一方式就是通过 shell 提供的文本命令行界面&#xff08;command line interface&#xff0c;CLI&#xff09;。CLI 只允许输入文本&#xff0c;而且只能显示文本和基本图形输出。由于此限制…

数据结构与算法-图论-最短路和其他的结合

介绍 最短路算法常与深度优先搜索&#xff08;DFS&#xff09;、动态规划&#xff08;DP&#xff09;、二分答案、拓扑排序等算法结合使用&#xff1a; - 最短路与DFS结合&#xff1a;在一些图的路径问题中&#xff0c;当需要访问特定的多个结点&#xff0c;且数据范围较小时…

RK3399 Android7双WiFi功能实现

在Android系统里面,WiFi功能STA和AP模式是互斥的,而现在越来越多的WiFi模组或者芯片能支持并发模式,即STA+P2P、STA+STA或者STA+AP模式组合。不管是单WiFi并发,还是双WiFi模组,想让STA和AP两个模式同时运行,对于Android7来说,是需要修改到系统源码,才能让APP层用Androi…