前言

数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。

也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。

此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。

欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。


等价类

提到等价类,学过离散数学的小伙伴一定不会感到陌生,等价类的定义建立在一个集合和其上的等价关系。这个关系满足自反性、对称性和传递性。依照这个关系可以将原集合中的元素划分成若干不相交子集,每个集合都是一个等价类

拿生活中常识举例来说,生活中常见的等价关系有亲戚关系,如果A和B是亲戚,那么B和A也是亲戚。如果A和B是亲戚,同时B和C是亲戚,那么A和C也是亲戚。

其实朋友关系也是一种等价关系。我们总说:“朋友的朋友还是朋友”。

数据结构中的等价类

数据结构中,也不乏有满足等价类所讨论的场景。无向图上的的连通关系就是一种等价关系,在无向图中,若两点间存在路径可以相互到达,那么这两个结点就是联通的。这个关系也满足等价类的定义,每个连通块就是一个等价类。

这个概念在后面最小生成树章节还会用到。

然而光讨论这些定义毕竟还只是数学理论层面,下面就来使用数据结构知识来解决等价性问题。

并查集

“用数据结构维护等价类,并查集是个不错的选择。”——沃茨基硕德

并查集用森林表示一个个不相交集的集合。每棵树就是一个集合,每个集合选择一个代表元。当需要查询两个元素是否在同一集合时,只需要查询其所在集合的代表元是否相同。而添加等价关系即代表将两个集合合并。

并查集应该支持两种操作:

  1. union(x,y)合并x和y所在的两个集合
  2. find(x),查询x所在集合的代表元

由于对于集合的操作需求仅有查询代表元,于是不妨使用有根树来表示一个等价集。
表示集合{1,3,2,5}\{1,3,2,5 \}{1,3,2,5}如下图所示:

当然树的形态不是唯一的,只要集合中的元素位于同一棵树中即可。

代表元可以选择树根,这样每个元素只需要不断地顺着父亲向上寻找即可找到代表元。同一集合中的元素寻找到的代表元一定相同,即集合中代表元唯一。

特别的,代表元的父亲指向自己:

这样,父节点为自身的结点就是代表元。

要合并两个集合,只需要将其中一个集合的代表元的父节点指向另一个集合的代表元。

例如和并两个集合{1,2,3,5}\{ 1,2,3,5\}{1,2,3,5}和{4,6,7}\{4,6,7\}{4,6,7}:

存储方式&封装

当前这个树结构,我们只关心每个结点的父节点,于是可以采取父连接的存储方式,即仅记录每个节点的父亲。

所以封装并查集只需要给出父数组和规模。
实例化一个并查集需要给出集合规模并申请对应大小的父节点数组空间,同时初始化父节点数组,使得每个点初始父节点为自身(自反性):

//C
typedef struct _Dsu{int * father;int size;
}Dsu;Dsu * createDsu(int size){Dsu * dsu = (Dsu*)malloc(sizeof(Dsu));dsu->size = size;dsu->father = (int*)malloc(sizeof(int) * (size + 1));for(int i = 1;i <= size;i++){dsu->father[i] = i;}return dsu;
}
//java
public class Dsu {private int[] father;private int size;public Dsu(int size) {this.size = size;father = new int[size + 1];for(int i = 1;i <= size;i++) {father[i] = i;}}
}

查找操作

查找一个元素所在集合的代表元思路就是一路向上寻找。

直到找到一个元素父节点是其本身,则该点为代表元。

这个过程可以使用递归实现:

//C
int find(Dsu * dsu,int k){if(k > dsu->size || k <= 0){return 0;}if(dsu->father[k] == k){return k;}return find(dsu,dsu->father[k]);
}
//java
public int find(int k) {if(k <= 0 || k > size){return 0;}if(father[k] == k) {return k;}return find(father[k]);
}

路径压缩

上面的算法确实能够很好地查询到一个元素所在集合的代表元。但仅是这样还是不够的,我们的需求是使得整个并查集在查询过程中尽可能的快。

设想如果并查集多次查询同一个元素,且这个元素与代表元相隔甚远,那么就会无故多走很多冤枉步骤。

由于我们只需要知道树中的根节点,根节点与元素之间的元素对答案没有任何影响。

所以一个直观的想法是将查询元素的父节点直接设置为代表元素。

其实这个想法还可以更大胆一些,如果将树种所有非根节点都变成根节点的儿子,那么查询每次就都是O(1)的。

但是这样一来维护就有了新的成本,所以不妨折中一下,仅在每次查询时将路径中将遇到结点的父指针指向根节点,即:


这个过程在刚才的递归中改造一下就可以顺带实现:

//C
int find(Dsu * dsu,int k){if(k > dsu->size || k <= 0){return 0;}if(dsu->father[k] == k){return k;}dsu->father[k] = find(dsu,dsu->father[k]);return dsu->father[k];
}
//java
public int find(int k) {if(k <= 0 || k > size){return 0;}if(father[k] == k) {return k;}father[k] = find(father[k]);return father[k];
}

合并操作

合并操作非常简单,只需要让一个集合代表元接入另一个集,通常是指向另一个集合的代表元。

//C
void merge(Dsu * dsu,int x,int y){dsu->father[find(x)]] = find(y);
}
//java
public void merge(int x,int y) {father[find(x)] = find(y);
}

需要特别注意的是,合并操作改变的是结点所在集合代表元的父指针,而非结点的父指针!

启发式合并

启发式合并可以在合并过程中优化整棵树的结构。

他的思路就是:“诶,你家的人比较少,你们搬到我家比较合适~”。

这个思路也就是我们所说的按秩合并,秩是元素所在集合元素的数量。

这样一来,除了基本的父节点数组外,还需要维护一个秩数组,不过也非常的简单:

typedef struct _Dsu{int * father;int * rank;int size;
}Dsu;Dsu * createDsu(int size){...for(int i = 1;i <= size;i++){dsu->rank[i] = 1;}...
}
//java
public class Dsu {private int[] father;private int[] rank;private int size;public Dsu(int size) {...for(int i = 1;i <= size;i++) {rank[i] = 1;}...}
}

合并的时候需要比较一下两个集合代表元秩的大小,合并后更新新的代表元的秩:

//C
void merge(Dsu * dsu,int x,int y){int fx = find(x);int fy = find(y);if(dsu->rank[fx] <= dsu->rank[fy]){//x所在集合秩较小dsu->father[fx] = fy;dsu->rank[fy] += dsu->rank[fx];}else{                            //y所在集合秩较小dsu->father[fy] = fx;dsu->rank[fx] += dsu->rank[fy];}
}
//java
public void merge(int x,int y) {int fx = find(x);int fy = find(y);if(rank[fx] <= rank[fy]) {father[fx] = fy;rank[fy] += rank[fx];}else {father[fy] = fx;rank[fx] += rank[fy];}
}

复杂度

对于空间复杂度,显然为O(n)

对于时间复杂度,如果不使用路径压缩或者启发式合并,最坏复杂度为O(nm)。而使用路径压缩或者启发式合并都可以将最坏复杂度降至O(mlogn)。

在实际的应用中,并不建议同时使用二者,这对降低复杂度没有进一步的帮助。对于基础的并查需求,使用路径压缩更为合适,其比较容易编写。对于不便于进行大量修改的需求中,则建议使用启发式合并。

并查集例题

来一道字节跳动经典面试题:

网络灾难字节跳动在全国各地都建立了服务器集群,各个服务器之间通过光缆连接形成了网状拓扑结构。
有一天邪恶的外星人入侵了集群,他们想破坏字节跳动的网络。
他们会残忍地切断一条一条网线,直到所有光缆都被切断。
现在给出这个拓扑结构以及外星人切断光缆的顺序,求当外星人切到第几根光缆时,字节跳动的网络会完全断裂成多块。
断裂成多块的定义为:当某一次光缆被切断后,若服务器A和服务器B完全没有光缆直接或间接连接,则称整个网络结构断裂成多块。输入描述第一行有两个数n m 表示字节跳动的服务器集群个数,以及光缆的数目。接下来m行 第1+x行 有两个数 a,b 表示服务器集群a 和 服务器集群b有光缆相连,该光缆编号为x。接下来还有m行 第1+m+x行 有一个数c,表示外星人在第x步会切断编号为c的光缆。
保证初始网络没有分裂。输出描述只有一个数 ans,表示外星人在第ans步切断光缆之后,字节跳动的网络会分裂成多个块。示例1
输入4    5
1    2
1    3
1    4
2    3
2    4
1
2
3
4
5输出3说明当1-2、1-3、1-4的光缆被切断后,服务器集群1 和 其他服务器集群完全断裂。

简单点来说,这个题目就是给定一张无向连通图,每次删除掉一条边。

问当删除到第几条边时整张图开始不连通?

这个问题从正面来看很难有头绪,因为每次删除一条边容易,但是再删除后如何判断,两个顶点仍处于同一集合确实难事。即使使用并查集也难已解决删边的问题。

但是如果将这个问题描述的过程反过来,角度就会大不相同:

即对一张没有边的图,每次加入一个边,何时该图连通?

问题瞬间就变得开朗起来了,我们只需要维护一个并查集,每次加边时判断是否需要并集,如果需要并集则等价类数量-1(开始为n)。当等价类数量为1时,整张图为连通图,结果就是最后加入的一条边。

//C
#include<stdio.h>
#include<malloc.h>typedef struct _Dsu{int * father;int size;
}Dsu;Dsu * createDsu(int size){Dsu * dsu = (Dsu*)malloc(sizeof(Dsu));dsu->size = size;dsu->father = (int*)malloc(sizeof(int) * (size + 1));for(int i = 1;i <= size;i++){dsu->father[i] = i;}return dsu;
}int find(Dsu * dsu,int k){if(k > dsu->size || k <= 0){return 0;}if(dsu->father[k] == k){return k;}dsu->father[k] = find(dsu,dsu->father[k]);return dsu->father[k];
}void merge(Dsu * dsu,int x,int y){dsu->father[find(x)]] = find(y);
}int edge[100050][2];
int idx[100050];
int main(){int n,m;scanf("%d%d",&n,&m);   //读入nmfor(int i = 0;i < m;i++){//读入所有边scanf("%d%d",&edge[i][0],&edge[i][1]);}for(int i = 1;i <= m;i++){//读入删边次序scanf("%d",&idx[i]);}Dsu * dsu = createDsu(n);int cnt = n;    for(int i = m,x,y;i > 0;i--){//逆序加边x = edge[idx[i]][0];y = edge[idx[i]][1];if(find(dsu,x) != find(dsu,y)){//如果新边两定点不在同一集合,则合并,等价类-1merge(dsu,x,y);cnt--;}if(cnt == 1){//如果等价类数量唯一,得到答案iprintf("%d",i);break;}}
}

往期博客

  • 【数据结构基础】数据结构基础概念
  • 【数据结构基础】线性数据结构——线性表概念 及 数组的封装
  • 【数据结构基础】线性数据结构——三种链表的总结及封装
  • 【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)
  • 【算法与数据结构基础】模式匹配问题与KMP算法
  • 【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)
  • 【数据结构与算法拓展】 单调队列原理及代码实现
  • 【数据结构基础】图的存储结构

参考资料:

  • 《数据结构》(刘大有,杨博等编著)
  • 《算法导论》(托马斯·科尔曼等编著)
  • 《图解数据结构——使用Java》(胡昭民著)
  • OI WiKi

【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)相关推荐

  1. python【数据结构与算法】并查集引入

    文章目录 1 并查集 2 策略 3 代码 1 并查集 Disjoint Set,实际上字面翻译是不相交的集合. 中文名 "并查集" 实际上源自其基本操作: union(X,Y):求 ...

  2. 数据结构与算法基础--错题集

    数据结构与算法的定义 数据结构是一门研究非数值计算的程序设计问题中的操作对象以及它们之间的关系和运算的学科. 算法是:解决问题的有限运算序列 算法分析的两个主要方面是:空间复杂度和时间复杂度 算法分析 ...

  3. 数据结构与算法--基础篇

    目录 概念 常见的数据结构 常见的算法 算法复杂度 空间复杂度 时间复杂度 数据结构与算法基础 线性表 数组 链表 栈 队列 散列表 递归 二分查找 概念 常见的数据结构 常见的算法 算法复杂度 空间 ...

  4. 【数据结构与算法基础】AOE网络与关键路径

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  5. 【数据结构与算法基础】最短路径问题

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  6. 【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  7. 【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  8. 【数据结构与算法基础】树与二叉树的互化

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  9. 数据结构与算法基础(java版)

    目录 数据结构与算法基础(java版) 1.1数据结构概述 1.2算法概述 2.1数组的基本使用 2.2 数组元素的添加 2.3数组元素的删除 2.4面向对象的数组 2.5查找算法之线性查找 2.6查 ...

最新文章

  1. 腾讯99公益日︱深圳市慈善会:那些无力的故事,都拥有了力量
  2. rust(56)-mp3(1)
  3. springboot集成redis配置多数据源
  4. 有关一百以内数字的Python算法
  5. 算法代码块总结(持续更新)
  6. Python自建collections模块
  7. Singleton(单件)--对象创建模式
  8. H3C s5500-SI-EI系列交换机 WEB界面登录配置
  9. 信息奥赛一本通(1325:【例7.4】 循环比赛日程表)
  10. 小蒜的A+B 计蒜客 - T1283
  11. Python列表和字典的本质和区别
  12. mysql rds 定时执行_RDS下执行SQL小脚本
  13. java过滤器是用来干什么的_java过滤器有什么作用
  14. excel导出动态表头以及二级三级表头,还有数据库动态的数据来源
  15. 如何登录锐捷设备(业务软件篇)
  16. C语言:计算班级平均数
  17. AW笔记本升级SSD,外接双屏中的一些注意事项
  18. 恢复出厂设置和格式化SD卡
  19. 阿拉伯数字跟中文汉字互转js
  20. WRF-Chem emission guide

热门文章

  1. 极限理论总结06:样本矩与样本中心距
  2. chisel 组合电路
  3. 晶振使浙大食堂在年后摇身一变成网红食堂
  4. salesforce 零基础开发入门学习(一)Salesforce功能介绍,IDE配置以及资源下载
  5. 演讲和会议的软件开发人员指南
  6. dao,dto,vo,pojo,bo
  7. 【DevPress】V2.5.2版本发布,悬浮窗设置及热门标签支持自定义配置,发布文章可同步到稀土掘金平台
  8. 移动端GIS功能开发
  9. C# 微信支付APIv3 SDK RSAUtility
  10. 如何把歌曲里的伴奏音乐提取出来,分享几个方法给大家!