10. 并查集

news2024/11/18 7:27:33

10. 并查集

并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作:

查询元素p和元素q是否属于同一组

合并元素p和元素q所在的组

10.1 并查集结构

并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:

  1. 每个元素都唯一的对应一个结点;

  2. 每一组数据中的多个元素都在同一颗树中;

  3. 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系

  4. 元素在树中并没有子父级关系的硬性要求;

10.2 并查集API设计

10.3 并查集的实现

10.3.1 UF(int N)构造方法实现

  1. 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组;

  2. 初始化数组eleAndGroup;

  3. 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点

所在的分组,那么初始化情况下,i索引处存储的值就是i

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aCzadvl-1682237162968)(null)]

10.3.2 union(int p,int q)合并方法实现

  1. 如果p和q已经在同一个分组中,则无需合并

  2. 如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识符即

  3. 分组数量-1

10.3.3 代码

package com.ynu.Java版算法.U10_并查集.T1_并查集实现;

public class Main {
    public static void main(String[] args) {

        // 创建10个元素的并查集
        UF uf = new UF(10);
        System.out.println(uf.count());

        // 把1,9合并到4
        uf.union(1,4);
        uf.union(9,4);

        // 合并2,3两组  把5合并到3
        uf.union(2,3);
        System.out.println(uf.count());

        // 合并5,3两组  把5合并到3
        uf.union(5,3);
        // 输出3组的所有元素
        System.out.println(uf.findAll(3));

        // 把4合并到3
        uf.union(4,3);
        System.out.println(uf.findAll(3));


    }
}

package com.ynu.Java版数据结构.U10_并查集.T1_并查集实现;

import java.util.ArrayList;
import java.util.List;

public class UF {

    // 记录节点元素和该元素所在分组的标识
    private int[] eleAndGroup;
    // 记录并查集中数据的分组个数
    private int count;

    //初始化并查集
    public UF(int N) {

        //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
        this.count = N;
        //初始化数组
        eleAndGroup = new int[N];

        // 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该
        // 结点所在的分组,那么初始化情况下,i索引处存储的值就是i
        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){
        return eleAndGroup[p];
    }

    // 把元素p和元素q所在的分组合并
    // p合并到q上
    public void union(int p,int q){

        if (connected(p,q)){
            System.out.println("p,q本来就在一个分组");
            return;
        }

        int pGroup = find(p);
        int qGroup = find(q);


        for (int i = 0; i < eleAndGroup.length; i++) {
            if (eleAndGroup[i]==pGroup){
                eleAndGroup[i] = qGroup;
            }
        }

        // 分组数量减1
        count--;


    }


    // 查找某一组的所有元素
    public List<Integer> findAll(int g){

        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < eleAndGroup.length; i++) {
            if (eleAndGroup[i]==g){
                res.add(i);
            }
        }

        return res;

    }


}

10.3.4 并查集应用举例

如果我们并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则我们就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?如果连通,则他们之间可以通信,如果不连通,则不能通信,此时我们又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。 一般像计算机这样网络型的数据,我们要求网络中的每两个数据之间都是相连通的,也就是说,我们需要调用很多次union方法,使得网络中所有数据相连,其实我们很容易可以得出,如果要让网络中的数据都相连,则我们至少要调用N-1次union方法才可以,但由于我们的union方法中使用for循环遍历了所有的元素,所以很明显,我们之前实现的合并算法的时间复杂度是O(N^2),如果要解决大规模问题,它是不合适的,所以我们需要对算法进行优化。

10.4 UF_Tree算法优化

​ 为了提升union算法的性能,我们需要重新设计fifind方法和union方法的实现,此时我们先需要对我们的之前数据结构中的eleAndGourp数组的含义进行重新设定:

1.我们仍然让eleAndGroup数组的索引作为某个结点的元素;

2.eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;

10.4.1 UF_Tree API设计

10.4.2 find(int p)查询方法实现

1.判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;

2.如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;

10.4.3 union(int p,int q)合并方法实现

  1. 找到p元素所在树的根结点

  2. 找到q元素所在树的根结点

  3. 如果p和q已经在同一个树中,则无需合并;

  4. 如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;

  5. 分组数量-1

10.4.4 代码

package com.ynu.Java版算法.U10_并查集.T2_UF_Tree算法优化;

public class UF_Tree {


    // 记录并查集中数据的分组个数
    private int count;

    // 记录元素和组别
    private int[] eleAndGroup;

    // 初始化元素个数为N的并查集
    public UF_Tree(int N) {

        eleAndGroup = new int[N];
        count = N;
        for (int i = 0; i < eleAndGroup.length; i++) {
            eleAndGroup[i] = i;
        }

    }

    // 获取组的个数
    public int getCount(){
        return count;
    }

    // 判断两个元素是否属于用一个组
    public boolean connected(int p,int q){
        return find(p)==find(q);
    }

    // 查找元素属于的组
    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){

        if (connected(p,q)){
            System.out.println("本来就在一组无需合并");
            return;
        }

        int pRoot = find(p);  // 找到p所在的组
        int qRoot = find(q);  // 找到q所在的组

        eleAndGroup[pRoot] = qRoot;  // 把p所在的组合并到q所在的组
        count--;   // 组数减一

    }



}

10.4.5 优化后的性能分析

​ 我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。 但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N):

10.5 路径压缩

​ UF_Tree中最坏情况下union算法的时间复杂度也较高,其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化find方法。 之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。

​ 只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高find方法的效率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码调整数组中的值。

1.5.1 UF_Tree_Weighted API设计

1.5.2 代码

package com.ynu.Java版算法.U10_并查集.T3_路径压缩;

public class UF_Tree_Weighted {
    //记录结点元素和该元素所的父结点
    private int[] eleAndGroup;
    //存储每个根结点对应的树中元素的个数
    private int[] sz;
    //记录并查集中数据的分组个数
    private int count;

    //初始化并查集
    public UF_Tree_Weighted(int N) {
        //初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
        this.count = N;
        //初始化数组
        eleAndGroup = new int[N];
        sz = new int[N];
        //把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点的父结点,那么初始化情况下,i索引处存储的值就是i
        for (int i = 0; i < N; i++) {
            eleAndGroup[i] = i;
        }
        //把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
        for (int i = 0; i < sz.length; i++) {
            sz[i] = 1;
        }
    }

    //获取当前并查集中的数据有多少个分组
    public int count() {
        return count;
    }

    //元素p所在分组的标识符
    public int find(int p) {
        while (true) {
            //判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
            if (p == eleAndGroup[p]) {
                return p;
            }
            //如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止;
            p = 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) {
        //找到p元素所在树的根结点
        int pRoot = find(p);
        //找到q元素所在树的根结点
        int qRoot = find(q);
        //如果p和q已经在同一个树中,则无需合并;
        if (pRoot == qRoot) {
            return;
        }
        //如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,把较小的树合并到较大的树上
        if (sz[pRoot] <= sz[qRoot]) {
            eleAndGroup[pRoot] = qRoot;
            //重新调整较大树的元素个数
            sz[qRoot] += sz[pRoot];
        } else {
            eleAndGroup[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
        //分组数量-1
        count--;
    }

}



package com.ynu.Java版算法.U10_并查集.T3_路径压缩;

public class Main {
    public static void main(String[] args) {

        UF_Tree_Weighted uf_tree_weighted = new UF_Tree_Weighted(10);

        // 合并1,2到3
        uf_tree_weighted.union(1,3);
        uf_tree_weighted.union(2,3);

        System.out.println(uf_tree_weighted.count());

        //合并 4 ,7到8
        uf_tree_weighted.union(4,8);
        uf_tree_weighted.union(7,8);

        System.out.println(uf_tree_weighted.count());

        //合并3组到9
        uf_tree_weighted.union(3,9);

        //查找1所在的组  应该是3
        System.out.println(uf_tree_weighted.find(1));
    }
}

10.6 畅通工程

​ 某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?

总共有20个城市,目前已经修改好了7条道路,问还需要修建多少条道路,才能让这20个城市之间全部相通?

解题思路:

1.创建一个并查集UF_Tree_Weighted(20);

2.分别调用union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8),表示已经修建好的

道路把对应的城市连接起来;

3.如果城市全部连接起来,那么并查集中剩余的分组数目为1,所有的城市都在一个树中,所以,只需要获取当前

并查集中剩余的数目,减去1,就是还需要修建的道路数目; 每修一条路,就少一个分组。

代码:

package com.ynu.Java版算法.U10_并查集.T4_畅通工程;

import com.ynu.Java版算法.U10_并查集.T3_路径压缩.UF_Tree_Weighted;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<List<Integer>> paths = new ArrayList<>();
        List<Integer> path1 = new ArrayList<>();
        path1.add(0);
        path1.add(2);
        List<Integer> path2 = new ArrayList<>();
        path2.add(6);
        path2.add(9);
        List<Integer> path3 = new ArrayList<>();
        path3.add(3);
        path3.add(8);
        List<Integer> path4 = new ArrayList<>();
        path4.add(5);
        path4.add(11);
        List<Integer> path5 = new ArrayList<>();
        path5.add(2);
        path5.add(12);
        List<Integer> path6 = new ArrayList<>();
        path6.add(6);
        path6.add(10);
        List<Integer> path7 = new ArrayList<>();
        path7.add(4);
        path7.add(8);
        paths.add(path1);
        paths.add(path2);
        paths.add(path3);
        paths.add(path4);
        paths.add(path5);
        paths.add(path6);
        paths.add(path7);
        UF_Tree_Weighted uf = new UF_Tree_Weighted(20);

        for (List<Integer> path : paths) {
            Integer v = path.get(0);
            Integer w = path.get(1);
            uf.union(v,w);
        }

        System.out.println("还需要"+ (uf.count() - 1 )+"条道路");

    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/451689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目风险管理的5个重点 不得不重视

风险管理持续贯穿软件项目的整个生命周期&#xff0c;其对项目的影响非常大&#xff0c;那么如何高效管理项目风险&#xff1f;5个风险管理重点如下&#xff1a; 1、风险识别和科学分析 需要对风险发生的可能性进行分析&#xff0c;判断风险对项目影响可能性并记录其特征&#…

【 初识 Spring MyBatis 查询数据库 】

文章目录 一、概念二、为什么学 MyBatis三、怎么学 MyBatis四、第⼀个MyBatis查询4.1 MyBatis 在整个框架中的定位4.2 准备&#xff1a;创建库和表4.3 配置 MyBatis 开发环境4.3.1 添加MyBatis框架⽀持4.3.1.1 ⽼项⽬添加支持扩展&#xff1a;在⽼项⽬中快速添加框架 - EditSta…

ChatGPT 速通手册——不同相似度算法的分值介绍

不同相似度算法的分值介绍 在信息大暴涨的今天&#xff0c;人类已经不可能出现通才、全才式的人物。利用 ChatGPT 来询问我们未知领域的知识是很好的习惯和用法。但对严肃知识的学习&#xff0c;一定要通过权威来源复核审校&#xff0c;保证自己所学知识的正确。否则&#xff…

【安全学习笔记】信息收集-CDN相关的技术(CDN绕过)

CDN相关的技术&#xff08;CDN绕过&#xff09; CDN&#xff1a;内容分发网络&#xff0c;它是构建在现有网络基础之上的智能虚拟网络&#xff0c;依靠部署在各地的边缘服务器&#xff0c;通过中心平台的负载均衡、内容分发、调度等功能模块&#xff0c;使用户就近获取所需内容…

104. 二叉树的最大深度【75】

难度等级&#xff1a;容易 上一篇算法&#xff1a; 101. 对称二叉树【74】 力扣此题地址&#xff1a; 104. 二叉树的最大深度 - 力扣&#xff08;Leetcode&#xff09; 1.题目&#xff1a;104. 二叉树的最大深度 给定一个二叉树&#xff0c;找出其最大深度。 二叉树的深度为根…

【并发编程】Java并发之关键字synchronized使用和原理

文章目录 前言一、synchronized的四种应用方式修饰一个代码块修饰一个方法修饰一个静态的方法修饰一个类 二、synchronized底层语义原理三、理解Java对象头与Monitor四、synchronized代码块底层原理五、synchronized方法底层原理六、Java虚拟机对synchronized的优化偏向锁轻量级…

Finetuner+:为企业实现大模型微调和私有化部署

如 ChatGPT、GPT4 这样的大型语言模型就像是你为公司请的一个牛人顾问&#xff0c;他在 OpenAI、Google 等大公司被预训练了不少的行业内专业知识&#xff0c;所以加入你的公司后&#xff0c;你只需要输入 Prompt 给他&#xff0c; 介绍一些业务上的背景知识&#xff0c;他就能…

清除Github提交历史commit

如果提交代码到Github仓库时&#xff0c;不小心把敏感信息&#xff08;比如登陆账号和登陆密码&#xff09;提交了上去&#xff0c;尽快处理。 git log 查看提交记录 git log定位你误操作的那一个版本 ‘be757abcb2b6c2b86b489384aeb4619d9b8c94c7’ 比如这个是你提交版本的…

2023全网汇总PMP备考攻略(附答题技巧)

一&#xff0c;多复习和学习新版考纲 01《PMBOK》看三遍 这边建议看三遍《PMBOK》&#xff0c;更有利于我们巩固知识&#xff0c;查缺补漏。 第一遍 第一遍是老师带着我们去看。这个时候一定要非常专心&#xff0c;千万不要上课走神或者玩手机。因为这一遍老师会告诉我们&a…

FreeRTOS源码获取以及解释各个文件作用

1.源码可以在官网:FreeRTOS官网&#xff1a;https://www.freertos.org/下载 2.FreeRTOS源码内容介绍 名称 描述 FreeRTOS FreeRTOS内核 FreeRTOS-Plus FreeRTOS组件 tools 工具 GitHub-FreeRTOS-Home FreeRTOS的GitHub仓库链接 Quick_Start_Guide 快速入门指南官方…

C/C++ Linux进程操作

目录 一、简介 二、创建进程 1. fork 2. wait 3. exit 三、多进程高并发设计 四、孤儿进程 五、僵尸进程 六、守护进程 七、总结 一、简介 进程是什么&#xff1f; 答&#xff1a;可以简单理解为&#xff0c;一个 .exe 的应用程序&#xff0c;就是运行在进程中的&a…

ChatGPT时代,我们可能站到了自然语言编程的大门口

ChatGPT大火&#xff0c;我现在有种感觉&#xff1a;我们可能站到了自然语言编程的门口&#xff0c;一脚下去&#xff0c;也许能把门踹开。 当然&#xff0c;也可能会踢到一块铁板。 回顾我们的编程之路&#xff0c;基本上就是一个编程门槛不断降低的历史。 最早的一批前辈们…

OSCP-Fail(rsync、fail2ban提权)

目录 扫描 rsync 提权 扫描 rsync 基于nmap,确信将进一步研究rsync。 为此,将使用netcat使用的rsync枚举。 使用netcat,我们可以列出rsync托管的当前共享。 我们看到“fox”和“fox home

万字长文带我弄懂JS基础!!!

js的学习笔记 文章目录 js的学习笔记JS基础篇js 的输出js的基本语法js语句js的注释js的变量js数据类型简介js对象简介js函数简介js的作用域js中的事件js字符串js运算符js条件语句js循环for/in循环 js的break和continuejs之typedefnullundefinedjs类型转换**constructor属性**St…

人类思维VS AI智能:谁是未来的胜者?

在“人工智能&#xff08;AI&#xff09;是否会取代人类”的问题上&#xff0c;谷歌的首席执行官埃德拉里博斯&#xff08;Ed Larrabee&#xff09;说&#xff1a;“我不认为 AI会取代人类。”而英国首相鲍里斯约翰逊则认为&#xff1a;“我们不能让 AI成为我们的敌人。”现在&…

MySQL解压版安装步骤

百度网盘有安装版、解压包安装包以及visual插件 链接&#xff1a;https://pan.baidu.com/s/1XXvWa40FYX5mtqofW_knIg 提取码&#xff1a;ky2q 下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 解压压缩包&#xff0c;进入bin目录&#xff0c;地址栏输…

从C出发 26 --- 指针 : 一种特殊的变量

指针是变量&#xff0c; 是特殊的变量 在计算机内部逻辑上是一个一个存储单元&#xff0c;每个存储单元是一个字节 8 G /16 G 表示的是存储单元的数量 如果要确定某一个具体的存储单元&#xff0c;要怎么办&#xff1f; 可以编号&#xff0c;这里的 0 1 2 3 指的就是内存地…

如何利用TURF分析来对餐厅菜品进行组合搭配?

1.数据源说明 1.1 数据简单说明 本数据源采用的是某餐厅8月份的销售明细表。本文会主要用到一下字段值&#xff1a; order_id&#xff0c; 产品订单号dishes_name&#xff0c;菜品名称counts, 消费数量amounts&#xff0c;消费金额 1.2 数据截图 以下是数据源的截图 1.3…

【移动端网页布局】移动端网页布局基础概念 ⑤ ( 视网膜屏技术 | 二倍图概念 | 代码示例 )

文章目录 一、视网膜屏技术二、二倍图概念三、代码示例 一、视网膜屏技术 PC 端 和 早期的 移动端 网页中 , CSS 中配置的 1 像素 对应的就是物理屏幕中的 1 像素 ; Retina 视网膜屏幕 技术出现后 , 将多个物理像素压缩到一块屏幕中 , 可以达到更高的分辨率 , 画面显示效果更好…

【Linux】磁盘与文件系统

目录 一、磁盘的物理结构 二、磁盘逻辑抽象 三、文件系统 1、Super Block 2、Group Descriptor Table 3、inode Table 4、Data Blocks 5、inode Bitmap 6、Block Bitmap 四、Linux下文件系统 1、inode与文件名 2、文件的增删查改 2.1、查看文件内容 2.2、删除文件…