前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
等价类
提到等价类,学过离散数学的小伙伴一定不会感到陌生,等价类的定义建立在一个集合和其上的等价关系。这个关系满足自反性、对称性和传递性。依照这个关系可以将原集合中的元素划分成若干不相交子集,每个集合都是一个等价类
拿生活中常识举例来说,生活中常见的等价关系有亲戚关系,如果A和B是亲戚,那么B和A也是亲戚。如果A和B是亲戚,同时B和C是亲戚,那么A和C也是亲戚。
其实朋友关系也是一种等价关系。我们总说:“朋友的朋友还是朋友”。
数据结构中的等价类
数据结构中,也不乏有满足等价类所讨论的场景。无向图上的的连通关系就是一种等价关系,在无向图中,若两点间存在路径可以相互到达,那么这两个结点就是联通的。这个关系也满足等价类的定义,每个连通块就是一个等价类。
这个概念在后面最小生成树章节还会用到。
然而光讨论这些定义毕竟还只是数学理论层面,下面就来使用数据结构知识来解决等价性问题。
并查集
“用数据结构维护等价类,并查集是个不错的选择。”
——沃茨基硕德
并查集用森林表示一个个不相交集的集合。每棵树就是一个集合,每个集合选择一个代表元。当需要查询两个元素是否在同一集合时,只需要查询其所在集合的代表元是否相同。而添加等价关系即代表将两个集合合并。
并查集应该支持两种操作:
- union(x,y)合并x和y所在的两个集合
- 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 =