• 数据结构与算法分析之并查集


    1. 并查集

    • 并查集是一种树型的数据结构,并查集可以高效的进行如下操作:
      • 查询元素p和元素q是否属于同一组
      • 合并元素p和元素q所在的组
        在这里插入图片描述

    1.1 并查集结构

    • 并查集是一种树型结构,但这棵树和我们之前学的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:
      • 每个元素都唯一的对应一个结点
      • 每一组数据中的多个元素都在同一棵树中
      • 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系
      • 元素在树中并没有子父级关系的硬性要求
        在这里插入图片描述

    1.2 并查集API设计

    在这里插入图片描述

    1.3 并查集实现

    1.3.1 UF(int N)构造方法实现
    • 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
    • 初始化数组eleAndGroup
    • 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化的情况下,i索引处存储的值就是i
      在这里插入图片描述
    1.3.2 union(int p, int q)合并方法实现
    • 如果p和q已经在同一个分组中,则无需合并
    • 如果p和q不在同一个分组,则只需要将p元素所在的组的所有的元素的组标识符修改为q元素所在组的标识符即可
    • 分组数量-1
      在这里插入图片描述
    1.3.3 代码实现
    package com.tiger.study.DataStructure.uf;
    
    public class UF {
        // 记录结点元素和改元素所在分组的标识
        private int[] eleAndGroup;
    
        // 记录并查集中数据的分组个数
        private int count;
    
        // 初始化并查集
        public UF(int N) {
            // 初始化分组的数量,默认情况下有N个数组
            this.count = N;
    
            // 初始化eleAndGroup数组
            this.eleAndGroup = new int[N];
    
            // 初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集每个索引处的值(该元素所在的组的标识符)就是该索引
            for (int i = 0; i < eleAndGroup.length; i++) {
                eleAndGroup[i] = i;
            }
    
        }
    
        // 获取当前并查集中有多少个分组
        public int count() {
            return count;
        }
    
        // 元素p所在的分组的标识符
        public int find(int p) {
            return eleAndGroup[p];
        }
    
        // 判断并查集中元素p和元素q是否是在同一个分组中
        public boolean connected(int p, int q) {
            return find(p) == find(q);
        }
    
        // 把p元素所在分组和q元素所在分组合并
        public void union(int p, int q) {
            // 判断元素q和p是否在同一分组中,如果在结束
            if (connected(p, q)) {
                return;
            }
    
            // 找到p所在的标识符
            int pGroup = find(p);
    
            // 找到q所在的标识符
            int qGroup = find(q);
    
            // 合并组:让p所在组的所有元素的组的标识符变为q所在分组的标识符
            for (int i = 0; i < eleAndGroup.length; i++) {
                if (eleAndGroup[i] == pGroup) {
                    eleAndGroup[i] = qGroup;
                }
            }
    
            this.count--;
    
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    1.4 并查集应用举例

    • 如果我们并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则我们就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?如果连通,则他们之间可以通信,如果不连通,则不能通信,此时我们又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。

    1.5 UF_Tree算法优化

    • 为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们之前数据结构中的eleAndGroup数组的含义进行重新设定
      • 我们仍然让eleAndGroup数组的索引作为某个结点的元素
      • eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该节点的父结点
        在这里插入图片描述
    1.5.1 UF_Tree API设计在这里插入图片描述
    1.5.2 find(int p)查询方法实现
    • 判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了
    • 如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点直到找到根结点为止
      在这里插入图片描述
    1.5.3 union(int p, int q)合并方法实现
    • 找到p元素所在树的根结点
    • 找到q元素的根结点
    • 如果p和q已经在同一个树中,则无需合并
    • 如果p和q不在同一个分组,则只需要将p元素所在树根结点设置为q元素的根结点即可
    • 分组数量-1
      在这里插入图片描述
    1.5.4 代码实现
    package com.tiger.study.DataStructure.uf;
    
    public class UF_Tree {
        // 记录结点元素和该结点所在分组的标识
        private int[] eleAndGroup;
    
        // 记录并查集中的数据分组的个数
        private int count;
    
        // 初始化并查集
        public UF_Tree(int N) {
            // 初始化分组的数量,默认情况下有N个分组
            this.count = N;
    
            // 初始化分组的数量,默认情况下,有N个分组
            this.count = N;
    
            // 初始化eleAndGroup数组
            this.eleAndGroup = new int[N];
    
            // 初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集每个索引处的值(该元素所在的组的标识符)就是该索引
            for (int i = 0; i < eleAndGroup.length; i++) {
                eleAndGroup[i] = i;
            }
        }
    
        // 获取当前并查集中的数据有多少个分组
        public int count() {
            return count;
        }
    
        // 判断并查集中元素p和元素q是否是在同一个分组中
        public boolean connected(int p, int q) {
            return find(p) == find(q);
        }
    
        // 元素p所在的分组的标识符
        public int find(int p) {
            while (true) {
                if (p == eleAndGroup[p]) {
                    return p;
                }
                p = eleAndGroup[p];
            }
        }
    
        // 把p元素所在分组和q元素所在分组合并
        public void union(int p, int q) {
            // 找到p和q所在组对应树的的根结点
            int pRoot = find(p);
            int qRoot = find(q);
    
            // 如果p和q已经在同一分组,则不需要合并了
            if (pRoot == qRoot) {
                return;
            }
    
            // 让p所在树的根结点的父结点为q所在树的根结点即可
            eleAndGroup[pRoot] = qRoot;
            this.count--;
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    1.5.5 路径压缩
    • 之前我们这哎union算法中,合并树时将任意的一棵树连接到另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后再每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高find方法的效率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码调整数组中的值
      在这里插入图片描述
    1.5.6 UF_Tree_Weighted API设计

    在这里插入图片描述

    1.5.7 代码实现
    package com.tiger.study.DataStructure.uf;
    
    public class UF_Tree_Weighted {
        // 记录结点元素和该结点所在分组的标识
        private int[] eleAndGroup;
    
        // 记录并查集中的数据分组的个数
        private int count;
    
        // 用来存储每一个根结点对应的树中保存的结点的个数
        private int[] sz;
    
        // 初始化并查集
        public UF_Tree_Weighted(int N) {
            // 初始化分组的数量,默认情况下有N个分组
            this.count = N;
    
            // 初始化分组的数量,默认情况下,有N个分组
            this.count = N;
    
            // 初始化eleAndGroup数组
            this.eleAndGroup = new int[N];
    
            // 默认情况下sz中每一个元素的值都是1
            this.sz = new int[N];
            for (int i = 0; i < sz.length; i++) {
                sz[i] = 1;
            }
    
            // 初始化eleAndGroup中的元素及其所在的组的标识符,让eleAndGroup数组的索引作为并查集每个索引处的值(该元素所在的组的标识符)就是该索引
            for (int i = 0; i < eleAndGroup.length; i++) {
                eleAndGroup[i] = i;
            }
        }
    
        // 获取当前并查集中的数据有多少个分组
        public int count() {
            return count;
        }
    
        // 判断并查集中元素p和元素q是否是在同一个分组中
        public boolean connected(int p, int q) {
            return find(p) == find(q);
        }
    
        // 元素p所在的分组的标识符
        public int find(int p) {
            while (true) {
                if (p == eleAndGroup[p]) {
                    return p;
                }
                p = eleAndGroup[p];
            }
        }
    
        // 把p元素所在分组和q元素所在分组合并
        public void union(int p, int q) {
            // 找到p和q所在组对应树的的根结点
            int pRoot = find(p);
            int qRoot = find(q);
    
            // 如果p和q已经在同一分组,则不需要合并了
            if (pRoot == qRoot) {
                return;
            }
    
            // 判断pRoot对应的树大,还是qRoot对应的树大,最终需要把较小的树合并到较大的树中
            if (sz[pRoot] < sz[qRoot]) {
                eleAndGroup[pRoot] = qRoot;
                sz[qRoot] += sz[pRoot];
            } else {
                eleAndGroup[qRoot] = pRoot;
                sz[pRoot] += sz[qRoot];
            }
            this.count--;
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    二. 案例分析

    • 需求:某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程"的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
    • 在我们的测试数据文件夹中有一个trffic_project.txt文件,它就是诚征道路统计表,下面是对数据的解释:
      在这里插入图片描述
    • 思路
      • 创建一个并查集UF_Tree_Weighted(20)
      • 分别调用union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经修建好的道路把对应的城市连接起来;
      • 如果城市全部连接起来,那么并查集中剩余的分组数目为1,所有的城市都在一个树中,所以,只需要获取当前并查集中剩余的数目,减去1,就是还需要修建的道路数目;
    • 代码实现
    package com.tiger.study.DataStructure.Test.UF;
    
    import com.tiger.study.DataStructure.uf.UF_Tree_Weighted;
    
    import java.io.BufferedReader;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class Traffic_Project_Test {
        public static void main(String[] args) throws IOException {
            // 构建一个缓冲读取流BufferReader
            BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\Study_Code\\src\\main\\java\\com\\tiger\\study\\DataStructure\\Test\\UF\\traffic_project.txt")));
    
            // 读取第一行数据20
            int totalNumber = Integer.parseInt(br.readLine());
    
            // 构建并查集对象
            UF_Tree_Weighted uf = new UF_Tree_Weighted(totalNumber);
    
            // 读取第二行数据7代表已经修好了多少条到了
            int roadNumber = Integer.parseInt(br.readLine());
    
            // 循环读取7条道路
            for (int i = 0; i < roadNumber; i++) {
                String line = br.readLine();
                String[] str = line.split(" ");
                int p = Integer.parseInt(str[0]);
                int q = Integer.parseInt(str[1]);
                // 调用并查集对象union方法让两个城市相通
                uf.union(p, q);
            }
    
            // 获取当前并查集分组的数量-1就可以得到还需要修建的道路的数量
            int roads = uf.count() - 1;
            System.out.println("还需要修建" + roads + "条道路才能实现畅通工程。");
    
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
  • 相关阅读:
    如何解压 GZ 文件
    【C进阶】之结构体嵌套及对齐
    [unity3d][通过代码]让模型移动,动态改变模型位置,点对点移动
    仅需10秒!ChatGPT轻松画出UML用例图,我却苦战10分钟。
    python 语音识别(百度api)
    Linux-centos8安装docker
    【初阶数据结构】栈和队列——C语言(详解)
    多级缓存之缓存同步
    任务调度器之Azkaban的使用
    基于形态学滤波的心电信号ECG处理(MATLAB 2021B)
  • 原文地址:https://blog.csdn.net/qq_38689352/article/details/127551661