数据结构——并查集

文章目录

  • [1 基本介绍](#1 基本介绍)
  • [2 基本操作](#2 基本操作)
    • [2.1 查找 ( Find )](#2.1 查找 ( Find ))
      • [2.1.1 定义](#2.1.1 定义)
      • [2.1.2 实现思路](#2.1.2 实现思路)
      • [2.1.3 举例](#2.1.3 举例)
    • [2.2 合并 ( Union )](#2.2 合并 ( Union ))
      • [2.2.1 定义](#2.2.1 定义)
      • [2.2.2 实现思路](#2.2.2 实现思路)
      • [2.2.3 举例](#2.2.3 举例)
  • [3 基础实现](#3 基础实现)
  • [4 性能优化](#4 性能优化)
    • [4.1 路径压缩](#4.1 路径压缩)
      • [4.1.1 核心思想](#4.1.1 核心思想)
      • [4.1.2 实现方式](#4.1.2 实现方式)
    • [4.2 按秩合并](#4.2 按秩合并)
      • [4.2.1 核心思想](#4.2.1 核心思想)
      • [4.2.2 实现方式](#4.2.2 实现方式)
  • [5 优化后的实现](#5 优化后的实现)
  • [6 适用场景](#6 适用场景)
    • [6.1 社交网络中的好友关系](#6.1 社交网络中的好友关系)
    • [6.2 地图中的路径判断](#6.2 地图中的路径判断)
    • [6.3 互联网网络连接](#6.3 互联网网络连接)
    • [6.4 岛屿数量统计与区域合并](#6.4 岛屿数量统计与区域合并)
    • [6.5 等式方程的可满足性](#6.5 等式方程的可满足性)
  • [7 总结](#7 总结)

1 基本介绍

并查集 (Union-Find)是一种 树型 的数据结构,用于处理一些不交集的 查询合并 问题。表面上它很高大上,但是实现起来倒不算难。

它可以拿如下这张图来理解(下图中有两个不同的集合,一个集合以 1 为根节点,另一个集合以 7 为根节点):

2 基本操作

2.1 查找 ( Find )

2.1.1 定义

获取一个节点的根节点。它经常被用来检查两个节点是否属于同一子集。

2.1.2 实现思路

如果该元素就是根节点,则直接返回该元素自己;否则就查找该元素的父节点的根节点。

2.1.3 举例

对于基本介绍中的并查集,查找节点 1, 2, 3, 4, 5 的根节点的结果为节点 1;查找节点 6, 7, 8, 9, 10, 11 的根节点的结果为节点 7

2.2 合并 ( Union )

2.2.1 定义

将两个子集合并成同一个集合。

2.2.2 实现思路

让一个子集的根节点 指向 另一个子集的根节点。

2.2.3 举例

对于基本介绍中的并查集,如果将节点 2, 7 所在的子集合并(此处让 1 指向 7),则会得到如下的并查集:

3 基础实现

java 复制代码
public class UnionFind {
    private final int[] parent; // parent[i] 表示第 i 个元素的父节点

    // 初始化并查集
    public UnionFind(int size) {
        parent = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i; // 初始时,每个元素的父节点都是自己
        }
    }

    // 查找 x 的根节点
    public int find(int x) {
        if (x != parent[x]) { // 如果 x 不是根节点
            return find(parent[x]); // 则寻找 x 的根节点并返回
        }
        return x; // 否则直接返回 x 自己
    }

    // 合并两个节点所在的集合
    public void union(int x, int y) {
        parent[find(x)] = find(y); // 让 x 的根节点 指向 y 的根节点
    }

    // 测试用例
    public static void main(String[] args) {
        UnionFind uf = new UnionFind(10);
        uf.union(1, 2);
        uf.union(3, 4);
        uf.union(2, 3);

        System.out.println(uf.find(1) == uf.find(4)); // 输出 true,因为 1 和 4 现在属于同一个集合
    }
}

注意,本文的实现假设元素是从 0size - 1 的连续整数。如果需要处理 非连续自定义类型 的元素,可以使用映射(如 HashMap)来将元素映射到这些整数索引上。

4 性能优化

以下给出两种主要的优化思路:

4.1 路径压缩

4.1.1 核心思想

在查找过程中,将路径上的所有节点都直接连接到根节点上。从而减少树的高度,加快后续查找速度。

4.1.2 实现方式

在执行查找操作时,如果当前节点的父节点不是根节点,则先将父节点连接到根节点上,然后再继续向上查找。

4.2 按秩合并

4.2.1 核心思想

(Rank)可以理解为 树的高度的一个上界。在合并两个集合时,将 秩较小的集合 合并到 秩较大的集合 中,以保持树的相对平衡。

4.2.2 实现方式

  • 在并查集的每个节点上添加一个额外的属性(如 rank),用于记录以该节点为根的子树的高度上界。
  • 在合并操作时,比较两个集合的秩,将秩较小的集合合并到秩较大的集合中,并更新合并后集合的秩(如果两个集合的秩相等,则合并后集合的秩加 1)。

5 优化后的实现

java 复制代码
public class UnionFind {
    private final int[] parent; // parent[i] 表示第 i 个元素的父节点
    private final int[] rank; // rank[i] 表示以 i 为根的树的深度(或节点数)

    // 初始化并查集
    public UnionFind(int size) {
        parent = new int[size];
        rank = new int[size];
        for (int i = 0; i < size; i++) {
            parent[i] = i; // 初始时,每个元素的父节点都是自己
            rank[i] = 1;   // 初始时,每个元素都是独立的树,深度为 1
        }
    }

    // 查找 x 的根节点
    public int find(int x) {
        if (x != parent[x]) { // 如果 x 不是根节点,先更新 x 的根节点
            // 则使用路径压缩,将 x 的 父节点 直接指向 根节点
            parent[x] = find(parent[x]);
        }
        return parent[x]; // 然后返回 x 的根节点
    }

    // 合并两个节点所在的集合
    public void union(int x, int y) {
        int rootX = find(x); // 获取 x 的根节点
        int rootY = find(y); // 获取 y 的根节点
        if (rootX != rootY) { // 只有在两个根节点不是同一个节点时才进行如下操作
            // 按秩合并:将 秩较小的集合 合并到 秩较大的集合
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else { // 如果两个集合的秩相等
                parent[rootY] = rootX; // 则随意合并
                rank[rootX]++; // 并让被合并的集合的秩加一
            }
        }
    }

    // 测试用例
    public static void main(String[] args) {
        UnionFind uf = new UnionFind(10);
        uf.union(1, 2);
        uf.union(3, 4);
        uf.union(2, 3);

        System.out.println(uf.find(1) == uf.find(4)); // 输出 true,因为 1 和 4 现在属于同一个集合
    }
}

6 适用场景

6.1 社交网络中的好友关系

  • 应用场景:在社交网络中,人与人之间存在着好友关系。通过并查集可以方便地建立和查询好友圈关系。
  • 实现方式:将每个人看作一个节点,利用并查集建立这些节点之间的关系。当两个人成为好友时,将它们所在的两个集合进行合并操作。通过查询某两个人是否属于同一个集合,就可以判断他们是否在同一个好友圈中。

6.2 地图中的路径判断

  • 应用场景:在电子地图中,经常需要判断两个地点之间是否存在路径。
  • 实现方式:将地图上的每个地点看作一个节点,利用并查集来表示各个地点的连接关系。当两个地点之间存在路径时,将它们所在的两个集合进行合并操作。通过查询两个地点是否属于同一个集合,就可以判断它们之间是否存在路径。

6.3 互联网网络连接

  • 应用场景:在互联网网络中,经常需要处理网络连接的问题。
  • 实现方式:将网络中的每个节点看作一个机器或者设备,利用并查集来表示网络中各个节点的连接关系。通过对两个节点进行合并操作,将它们所在的集合合并为一个集合,从而建立网络连接。通过查询两个节点是否属于同一个集合,就可以判断它们之间是否存在网络连接。

6.4 岛屿数量统计与区域合并

  • 应用场景:在地理学和计算机视觉领域,经常需要统计岛屿数量以及合并区域。
  • 实现方式:将地图上的每个陆地区域看作一个节点,利用并查集来表示各个区域的连通性。通过查询不同集合的数量,就可以统计岛屿的数量。同时,通过对区域进行合并操作,可以实现区域的合并。

6.5 等式方程的可满足性

  • 应用场景:给定一个由表示变量之间关系的字符串方程组成的数组,需要判断是否存在一种整数分配方式,使得所有给定的方程都得到满足。
  • 实现方式:使用并查集求解,先处理等号情况,将等号两边元素所在的集合并到一起;后处理不等号方程,使用查操作,找到不等号两边的元素是否为同一集合,若为同一集合则证明该方程不满足,返回false,若所有的不等号方程都满足,则返回true。

7 总结

并查集作为一种 简单但十分实用 的数据结构,在解决集合相关的问题上具有 很高的效率和便利性 。其应用场景不仅限于上述几个领域,还包括 动态连通性问题最小生成树算法 等多个方面。

相关推荐
哎呦没10 分钟前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
小爬虫程序猿14 分钟前
如何利用Python解析API返回的数据结构?
数据结构·数据库·python
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
pianmian14 小时前
python数据结构基础(7)
数据结构·算法
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟6 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity7 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天7 小时前
java的threadlocal为何内存泄漏
java