【数据结构与算法】第十三章:并查集(并查集结构、API设计、代码实现、算法优化、性能分析、路径压缩)

本文主要是介绍【数据结构与算法】第十三章:并查集(并查集结构、API设计、代码实现、算法优化、性能分析、路径压缩),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

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

在这里插入图片描述

13.1、并查集结构

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

  1. 每个元素都 唯一 的对应一个结点
  2. 每一组数据中的多个元素都在 同一颗树
  3. 一个组中的数据对应的树和另外一个组中的数据对应的 树之间没有任何联系
  4. 元素在树中并 没有子父级关系 的硬性要求

在这里插入图片描述

13.2、并查集API设计

在这里插入图片描述

13.3、并查集实现

1)UF(int N)构造方法

  1. 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
  2. 初始化数组eleAndGroup
  3. 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化情况下,i索引处存储的值就是i

在这里插入图片描述

2)union(int p,int q)合并方法

  1. 如果p和q已经在同一个分组中,则无需合并
  2. 如果p和q不在同一个分组,则只需要将p元素所在组的所有元素的组标识符修改为q元素所在组的标识符即可
  3. 分组数量-1

在这里插入图片描述

3)代码实现

package chapter13;/*** @author 土味儿* Date 2021/9/15* @version 1.0* 并查集*/
public class UnionAndFind {/*** 记录结点元素和该元素所在分组的标识*/private int[] eleAndGroup;/*** 记录并查集中数据的分组个数*/private int count;/*** 构造器** @param n*/public UnionAndFind(int n) {// 初始化分组的数量;默认情况下,有n个分组count = n;// 初始化eleAndGroup分组eleAndGroup = new int[n];// 把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该// 结点所在的分组,那么初始化情况下,i索引处存储的值就是ifor (int i = 0; i < n; i++) {eleAndGroup[i] = i;}}/*** 分组的数量** @return*/public int count() {return count;}/*** 判断元素p和q是否在同一个分组中** @param p* @param q* @return*/public boolean connected(int p, int q) {return find(p) == find(q);}/*** 获取元素p所在分组的标识符** @param p* @return*/public int find(int p) {return eleAndGroup[p];}/*** 把元素p合并到q所在的分组** @param p* @param q*/public void union(int p, int q) {// 判断p和q是否在同一个分组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;}}// 分组数量减1count--;}
}
    public static void main(String[] args) {Scanner sc = new Scanner(System.in);System.out.println("请输入并查集中分组数量:");int n = sc.nextInt();UnionAndFind uf = new UnionAndFind(n);while (true) {System.out.println("请输入第一个要合并的元素:");int p = sc.nextInt();System.out.println("每输入第二个要合并的元素:");int q = sc.nextInt();// 判断p、q是否在同一个分组if (uf.connected(p, q)) {System.out.println(p + "," + q + "已经在同一个分组了!");continue;}uf.union(p, q);System.out.println("现在分组个数:" + uf.count());}}
每输入第二个要合并的元素:
2
1,2已经在同一个分组了!
请输入第一个要合并的元素:
1
每输入第二个要合并的元素:
3
现在分组个数:3
请输入第一个要合并的元素:
2
每输入第二个要合并的元素:
3
2,3已经在同一个分组了!
请输入第一个要合并的元素:

4)应用举例

如果并查集存储的每一个整数表示的是一个大型计算机网络中的计算机,则就可以通过connected(int p,int q)来检测,该网络中的某两台计算机之间是否连通?

如果连通,则他们之间可以通信,如果不连通,则不能通信,此时又可以调用union(int p,int q)使得p和q之间连通,这样两台计算机之间就可以通信了。

一般像计算机这样网络型的数据,要求网络中的每两个数据之间都是相连通的,也就是说,需要调用很多次union方法,使得网络中所有数据相连,其实很容易可以得出,如果要让网络中的数据都相连,则至少要调用 N-1 次union方法才可以,但由于union方法中使用for循环遍历了所有的元素,所以很明显,之前实现的合并算法的时间复杂度是 O(N^2),如果要解决大规模问题,它是不合适的,所以需要对算法进行优化

5)UF_Tree算法优化

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

  1. 仍然让eleAndGroup数组的索引作为某个结点的元素
  2. eleAndGroup[i] 的值不再是当前结点所在的分组标识,而是该结点的 父结点

在这里插入图片描述

1、UF_Tree API设计

在这里插入图片描述

2、find(int p)查询方法
  • 判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了
  • 如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根结点为止

在这里插入图片描述

3、union(int p,int q)合并方法
  1. 找到p元素所在树的根结点
  2. 找到q元素所在树的根结点
  3. 如果p和q已经在同一个树中,则无需合并
  4. 如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可
  5. 分组数量-1

在这里插入图片描述

4、代码实现
package chapter13;/*** @author 土味儿* Date 2021/9/15* @version 1.0* 并查树*/
public class UFTree {/*** 记录节点元素和该元素的父节点*/private int[] eleAndGroup;/*** 记录并查集中数据的分组个数*/private int count;/*** 构造器** @param n*/public UFTree(int n) {// 初始化分组的数量;默认情况下,有n个分组count = n;// 初始化eleAndGroup分组eleAndGroup = new int[n];// 把eleAndGroup数组的索引看做是每个节点存储的元素,把eleAndGroup数组每个索引处的值看做是该// 节点的父节点,那么初始化情况下,i索引处存储的值就是ifor (int i = 0; i < n; i++) {eleAndGroup[i] = i;}}/*** 分组的数量** @return*/public int count() {return count;}/*** 判断元素p和q是否在同一个分组中** @param p* @param q* @return*/public boolean connected(int p, int q) {return find(p) == find(q);}/*** 获取元素p所在分组的标识符** @param p* @return*/public int find(int p) {// 循环查找while (true){// 判断当前元素p的父节点eleAndGroup[p]是不是自己if(p == eleAndGroup[p]){// 是自己,证明已经是根节点return p;}// 不是自已,从父结点开始继续查找p = eleAndGroup[p];}}/*** 把元素p合并到q所在的分组** @param p* @param q*/public void union(int p, int q) {// p元素所在分组的根节点int pRoot = find(p);// q元素所在分组的根节点int qRoot = find(q);// 判断是否是同一分组if(pRoot == qRoot){return;}// 把p的根节点的父结点设为q的根节点eleAndGroup[pRoot] = qRoot;// 分组数量减1count--;}
}
5、优化后的性能分析

优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)

但是这个算法仍然有问题,因为之前不仅修改了union算法,还修改了find算法。修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是 O(N)

在这里插入图片描述

在 union方法中调用了两次find方法,所以在 最坏情况 下union算法的时间复杂度仍然为 O(N^2)

6)路径压缩

UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果能够通过一些算法让合并时,生成的树的 深度尽可能的小,就可以优化find方法

之前在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度
这里说的小树大树指的是树中元素数量,不是树的高度,用另一个数组记录;初始值为1,合并后更新大树的元素数量:原来数量+小树的元素数量

在这里插入图片描述

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

1、UF_Tree_Weighted API设计

在这里插入图片描述

2、代码实现
package chapter13;/*** @author 土味儿* Date 2021/9/15* @version 1.0* 优化并查树* 减小树的高度* 小树合并到大树*/
public class UF_Tree_Weighted {/*** 记录节点元素和该元素的父节点*/private int[] eleAndGroup;/*** 记录并查集中数据的分组个数*/private int count;/*** 记录每个根节点对应的树中元素的数量*/private int sz[];/*** 构造器** @param n*/public UF_Tree_Weighted(int n) {// 初始化分组的数量;默认情况下,有n个分组count = n;// 初始化eleAndGroup分组eleAndGroup = new int[n];// 把eleAndGroup数组的索引看做是每个节点存储的元素,把eleAndGroup数组每个索引处的值看做是该// 节点的父节点,那么初始化情况下,i索引处存储的值就是ifor (int i = 0; i < n; i++) {eleAndGroup[i] = i;}// 初始时每个根结点中元素数量都是1sz = new int[n];for (int i = 0; i < sz.length; i++) {sz[i] = 1;}}/*** 分组的数量** @return*/public int count() {return count;}/*** 判断元素p和q是否在同一个分组中** @param p* @param q* @return*/public boolean connected(int p, int q) {return find(p) == find(q);}/*** 获取元素p所在分组的标识符** @param p* @return*/public int find(int p) {// 循环查找while (true){// 判断当前元素p的父节点eleAndGroup[p]是不是自己if(p == eleAndGroup[p]){// 是自己,证明已经是根节点return p;}// 不是自已,从父结点开始继续查找p = eleAndGroup[p];}}/*** 把元素p合并到q所在的分组** @param p* @param q*/public void union(int p, int q) {// p元素所在分组的根节点int pRoot = find(p);// q元素所在分组的根节点int qRoot = find(q);// 判断是否是同一分组if(pRoot == qRoot){return;}// 比较p、q所在树中元素个数,把小树合并到大树if(sz[pRoot] < sz[qRoot]){// 小树合并到大树eleAndGroup[pRoot] = qRoot;// 更新大树中元素数量:加上小树的数量sz[qRoot] += sz[pRoot];}else{// 小树合并到大树eleAndGroup[qRoot] = pRoot;// 更新大树中元素数量:加上小树的数量sz[pRoot] += sz[qRoot];}// 分组数量减1count--;}
}

7)案例-畅通工程

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

问最少还需要建设多少条道路?

在测试数据文件夹中有一个trffic_project.txt文件,它就是道路统计表,下面是对数据的解释:

在这里插入图片描述

总共有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 chapter13;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;/*** @author 土味儿* Date 2021/9/15* @version 1.0* 案例:畅通工程*/
public class Traffic_Project {public static void main(String[] args) throws IOException {// 创建缓冲输入流读取BufferedReaderBufferedReader br = new BufferedReader(new InputStreamReader(Traffic_Project.class.getClassLoader().getResourceAsStream("trffic_project.txt")));// 读取第一行数据:城市数量int totalNum = Integer.parseInt(br.readLine());// 构建一个并查集对象UF_Tree_Weighted uf = new UF_Tree_Weighted(totalNum);// 读取第二行数据:已修好的道路数量int roadNum = Integer.parseInt(br.readLine());// 循环读取已经修建好的道路,并调用union方法for (int i = 1;i<=roadNum;i++){String line = br.readLine();String[] str = line.split(" ");int p = Integer.parseInt(str[0]);int q = Integer.parseInt(str[1]);uf.union(p,q);}// 获取剩余的分组数量int need = uf.count();// 计算出还需要修建的道路System.out.println("还需要修建"+(need-1)+"道路,城市才能相通");}
}
还需要修建12道路,城市才能相通

在这里插入图片描述

这篇关于【数据结构与算法】第十三章:并查集(并查集结构、API设计、代码实现、算法优化、性能分析、路径压缩)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/942793

相关文章

图的顺序存储和链式存储实现

目录 一、顺序存储 有向图: 无向图 代码实现 二、链式存储 有向图 无向图 代码实现 一、顺序存储 主要用到的是一个二维数组,也就是矩阵,直接上栗子: 有向图: 若要储存如下这个有向图: 需要建立一个二维数组 注意: 1、每一行代表的是图的每一个顶点 2、某一行的这一列,代表当前顶点所连接的下一个结点 初始化整个二维数组为0,在二维数组中:

按键配合LDO实现开关功能

今天给大家分享一个学到的按键开关电路,适合没有足够空间给自锁开关的场景,既可以用于USB供电控制也可以用于电池供电控制。话不多说上电路图先。 核心任务就是通过按键控制LDO芯片的使能管脚的电平状态,这枚NCP芯片高电平使能,VBAT是来自USB(或者电池,下面省略说明)的5v供电,高电平使能EN引脚之后LDO输出3.3v。EN引脚平时被100kΩ电阻拉低,在D10导通时由3V_EN(GPIO

算法应用趣事

在学习算法的路上总会想要深刻的理解算法的意义,透彻的明白其中的精髓。在这样的学习探索中也让我了解到了几个有趣的算法故事。 在生活中,当我们需要拿起字典查寻一个字时,如"天" ,会自动的掠过前半部分翻到字典的T开头的内容进行查找。 翻看 黄页 通讯录我们也会有这样的习惯或是小技巧。那么在编程的设计中,我们会是怎样的状况呢?   请你在1-100中随机想出一个数,让我来猜你只需要告诉我 ,猜到

Ionic4实现手机滑动答题页面---轮播图切换设计

Ionic4实现手机滑动答题页面 前言概述小结 前言      接着上篇标签切换的实现咱们接着来看一个实际的应用.这是一个需要实现在手机端实现切换显示多个轮播图标签的应用需求. 概述      当用户进入页面后会默认显示一类题型,当点击按钮时根据需要切换到另一组轮播图给用户使用.在页面上代码设计如下: <ion-card class="cardexam" ><!-- 使

JS实现动态页码及分页导航

目录 前言内容小结 前言      最近的项目需要添加一个分页导航的功能,没有用网上封装好的文件。通过JS自己简单实现了效果。下面和大家分享一下。 内容 下图为首次加载后的效果,默认显示5页, 当点击下一页时将选中页面的页码置于中间 下面让我们来看看实现的代码 第一部分是在页面显示内容的处理, function SetListPage() {$.aj

C++的数据结构(一)

在计算机科学领域,数据结构扮演着至关重要的角色。它们为有效地存储、检索和操作数据提供了基础框架。C++作为一种高效且功能强大的编程语言,为数据结构的实现和应用提供了丰富的工具。本文将探讨C++数据结构的重要性,并通过示例展示其在数据结构中的应用。         一、C++数据结构的重要性         数据结构对于任何计算机程序来说都是至关重要的,因为它们决定了程序如何组织

详解AI算法作画原理

AI作画算法的原理主要基于深度学习和计算机视觉技术,特别是生成对抗网络(GANs)和卷积神经网络(CNNs)等模型。以下是AI作画算法原理的详细解释: 数据收集与处理: AI作画的第一步是收集大量的艺术作品作为训练数据。这些艺术作品可能来自各种来源,包括在线数据库、艺术博物馆、艺术家作品等。数据收集后,需要进行预处理,如图像缩放、裁剪、去噪、归一化等,以便于后续的模型训练。特征提取: 使用深度学

ASP.NET一个简单的媒体播放器的设计与实现

摘  要 本论文所描述的播放器是在Microsoft Visual Studio .NET 2003平台下利用Visual Basic.NET语言完成的。使用Visual Basic.NET提供的Windows Media Player控件以及文件处理,最终实现一款别致的,贴近用户操作习惯的媒体播放器。 该播放器实现了对WAV、MID、MP3、MOV等格式的多媒体文件的播放功能;实现了播放列表加

[Java基础要义] HashMap的设计原理和实现分析

HashMap在Java开发中有着非常重要的角色地位,每一个Java程序员都应该了解HashMap。     本文主要从源码角度来解析HashMap的设计思路,并且详细地阐述HashMap中的几个概念,并深入探讨HashMap的内部结构和实现细节,讨论HashMap的性能问题,并且在文中贯穿着一些关于HashMap常见问题的讨论。     读完本文,你会了解到:

[Java基础要义] Java语言中Object对象的hashCode()取值的底层算法是怎样实现的?

Java语言中,Object对象有个特殊的方法:hashcode(), hashcode()表示的是JVM虚拟机为这个Object对象分配的一个int类型的数值,JVM会使用对象的hashcode值来提高对HashMap、Hashtable哈希表存取对象的使用效率。       关于Object对象的hashCode()返回值,网上对它就是一个简单的描述:“JVM根据某种策略生成的”