并查集【数据结构与算法】【C语言版-笔记】

目录

一、需求分析

假设有n个互不相交的集合

◼问题1:给定某个集合中的一个元素,查找该元素属于哪个集合?

◼问题2:如何合并两个集合?

实例:
有n个村庄,有些村庄之间有连接的路,有些没有(有路连接的村庄视为同一个集合)
设计一个数据结构,能快速的执行下面两个操作
◼查询2个村庄之间是否有连接的路
◼连接两个的村庄

若使用数组、线性链表等其两者操作的复杂度至少是O(n),而并查集能够办到查询、连接的均摊时间复杂度都是O(log~2~n),非常适合解决这类"连接"相关的问题。

二、并查集

顾名思义,并查集是一种对集合以及集合里元素操作的结构,其中"并"就是合并,"查"就是查询某元素属于哪个集合,这也是它的两大核心操作

◼查找(Find):查找元素所在的集合(这里的集合指的是逻辑上(数学上)的集合而不是具体某个特定集合)
◼合并(Union):将两个元素所在的集合合并为一个集合

如何选择并查集的存储结构?从树的形状出发,我们知道一棵树上的叶子必定属于同一棵树(额,这好像是废话),把树看作集合,叶子作为集合的元素。给定一片叶子,找到它对应的树干(或根)就可以确定叶子的归属。数据结构中树的常用存储结构有三种方式:【双亲表示法,孩子表示法,孩子兄弟表示法】我们选择双亲表示法,因为这种设计方法很适合找到根结点,从而很容易确定元素所属集合。对于合并集合,我们只需要修改其中一个集合的parent指向另一个集合的 "根" 即可。

cpp 复制代码
双亲表示法:
给定一块连续的内存空间存货结点,给每个结点附设一个指针器,指示它的双亲结点在存储空间中的位置
其存储表示如下:
#define MAX_TREE_SIZE 20
typedef int TElemType;
typedef struct {
	TElemType num;
	int parent;
} TNode;
typedef struct {
	TNode nodes[MAX_TREE_SIZE];
	int length;//结点数
} PTree

//并查集的存储表示(为了方便直接使用数组(集合为数集),且num既是集合中的元素也是结点本身在存储空间中的位置)
typedef TNode unionFind[MAX_TREE_SIZE];

(备注:为了方便直接使用数组(集合为数集),且num即是集合中的元素也是结点本身在存储空间中的位置)

三、代码实现

规定:用一块连续的内存空间存储n个集合的全部元素,每个集合对应一棵树,树的根结点的parent值记为 -1

3.1 Find函数

cpp 复制代码
//查找元素e所属的集合,即找到该集合的"根"元素,返回根元素
TElemType* Find(unionFind uf,TElemType e){
	int root=e;//当前元素本身
	int x=uf[e].parent;//e的双亲所在的位置
	while(x!=-1){
		root=x;
		x=uf[x].parent;
	}
	return root;
}

时间复杂度

最坏:O(n)、平均:O(log~2~n)、最好:O(1)

3.2 Union函数

cpp 复制代码
//合并两个集合,rooti(i=1,2)为集合的根
void Union(unionFind uf,int root1,int root2){
	if(root1==root2) return;//如果是同一个集合,不用合并
	uf[root2].parent=root1;
}

时间复杂度O(1)

3.3 优化1

在进行Find的时,发现查找的长度和树的高度有关,且最坏时间复杂度是O(n),这时棵树变成了单支树(退化成为链表)。因此,可以从树高的角度出发减少树高从而达到优化Find的时间效率。优化思路是在每次Union的时候,尽可能的让树 "不长高",让小的树合并到大的树(树的大小指的是结点数量的多少)或者是让矮的树合并到高树上。下面采用第一种,这种方法需要有一个计数器保存一个集合(树)的大小,我们直接利用一个集合的"根"的parent来保存集合的大小,同时为了保留根的标识(parent为-1),parent==集合大小的相反数

cpp 复制代码
void Union(unionFind uf,root1,root2){
	if(uf[root1].parent<uf[root2].parent){
		uf[root1].parent+=uf[root2].parent;
		uf[root2].parent=root1;
	}else{
		uf[root2].parent+=uf[root1].parent;
		uf[root1].parent=root2;
	}n
	
}

优化后,Find最坏的时间复杂度变为:O(log~2~n)
(第二种方法---矮树合并到高树上,如何实现,大家可以自行思考)

3.4 终极优化2---压缩策略

所谓压缩是指压缩树的高度。我们对Find操作进行优化,当执行Find时,我们把传入的元素到根的路径上的结点(包含元素本身所在的结点)全部都直接挂在根的孩子位置上(如果已经是根的孩子了,则不用修改)。当再次查找该元素所属集合时,只需要常数级别的操作就可以完成查找。

cpp 复制代码
void Find(unionFind uf,TElemType e){
	int root=e;//当前元素本身
	int x=uf[e].parent;//当前元素的双亲位置
	//找到元素所在集合的"根"
	while(x>=0){
		root=x;
		x=uf[e].parent;
	}
	while(e!=root){
		int t=uf[e].parent;//t表示路径上元素的位置,t等价uf[uf[e].parent].num,前面黄字部分已表明
		uf[e].parent=root;
		e=uf[t].num;
	}
	return root;
}

如下图,调用Find后

优化后时间复杂度

最坏: O ( α ( n ) ) O(\alpha(n)) O(α(n)),其中, α ( n ) \alpha(n) α(n)是比log~2~n增长速度小的数量级

相关推荐
小懒编程日记2 小时前
【数据结构与算法】B树
java·数据结构·b树·算法
王俊山IT2 小时前
C++学习笔记----8、掌握类与对象(五)---- 嵌套类与类中枚举
开发语言·c++·笔记·学习
Beginner_bml2 小时前
C语言---链表
c语言·数据结构
闫铁娃2 小时前
二分解题的奇技淫巧都有哪些,你还不会吗?
c语言·数据结构·c++·算法·leetcode
Y_3_72 小时前
【回溯数独】有效的数独(medium)& 解数独(hard)
java·数据结构·windows·算法·dfs·回溯
问道飞鱼3 小时前
每日学习一个数据结构-默克尔树(Merkle Tree)
数据结构·学习·默克尔树
CV工程师小林3 小时前
【算法】DFS 系列之 穷举/暴搜/深搜/回溯/剪枝(下篇)
数据结构·c++·算法·leetcode·深度优先·剪枝
LearnTech_1234 小时前
【学习笔记】手写一个简单的 Spring MVC
笔记·学习·spring·spring mvc
-$_$-5 小时前
【LeetCode HOT 100】详细题解之二叉树篇
数据结构·算法·leetcode