目录

数据结构——并查集

文章目录

  • [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 总结

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

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
Java技术小馆3 分钟前
如何设计一个本地缓存
java·面试·架构
洋次郎的歌17 分钟前
我要成为数据结构与算法高手(三)之双向循环链表
数据结构
XuanXu1 小时前
Java AQS原理以及应用
java
风象南4 小时前
SpringBoot中6种自定义starter开发方法
java·spring boot·后端
mghio13 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室17 小时前
java日常开发笔记和开发问题记录
java
咖啡教室17 小时前
java练习项目记录笔记
java
鱼樱前端18 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea19 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq