目录
0.引例
在我们刚上大学的时候,假如一个宿舍10个人,分别来自广东、湖南、江西;一开始谁也不认识谁,于是广东的只和广东的一起玩,湖南的只和湖南的一起玩,江西的只和江西的一起玩,毕竟是老乡嘛~ ,于是一个宿舍整体被划分成了三个小团体。
为了表示方便,我们给10个人编号,从0开始,于是10个人的编号依次为 0~9:
于是,我们可以很直观的看出谁来自于哪里。
后来呢,湖南的小伙伴和江西的小伙伴因为共同的口味,都喜欢吃辣的东西,玩在了一块,于是这两个小集体组成了一个大的集体。
我们可以这样划分:
于是,我们可以简单直观的看出爱吃辣的有哪些同学,不爱吃辣的有哪些同学。
当我们划分好了集合之后,就能很方便的进行元素和集合之间、集合和集合之间的操作了,适合进行这些操作的数据结构就是并查集。
1.并查集的原理
原理:将n个不同的元素划分成一些不相交的集合,开始时,每个元素自成一个单元素集合,然后按一定的规律将属于同一组元素的集合合并;在此过程中要反复用到查询某一个元素归属于那个集合的运算。(适合描述这种问题的抽象数据结构就叫做 —— 并查集)
简单来说,并查集就是能够快速判断一个元素位于哪个集合中,并且能够根据指定条件将不同的集合进行合并的这么一种数据结构。比如上述引例中,根据地域将10个单集合合并成了三个集合,根据是否爱吃辣,把湖南和江西的同学合并成了一个集合。反之,划分好集合之后,我们也可以快速判断一个同学是否爱吃辣?来自哪里?
2.并查集的存储结构
要想实现一个并查集,首先需要确定其底层存储数据的结构,我们使用顺序表,也就是数组来存储。并查集的底层结构和堆类似,用下标表示当前结点和双亲结点之间的关系(重要)。
使用数组存储的理由如下:
- 使用并查集的时候需要对每个元素从0开始编号,而数组天然就是从0开始编号的结构。
- 数组支持随机访问,查找效率高。
数组各字段的含义:
- 数组的下标对应集合中元素的编号
- 数组中如果为负数,负号代表根,数字代表该集合中元素个数(一般选择编号最小的为根)
- 数组中如果为非负数,代表该元素双亲在数组中的下标
以引例为例说明一下:
- 一开始,为每个元素从0开始编号,并且每个元素都是一个集合,并且都代表该集合的根。
- 把引例中的集合转化为并查集的形式
- 用树形结构表示一下各个集合,更加直观
3.并查集的实现
并查集接口总览
首先需要明确的是,使用并查集的时候,我们为每个要存储的数据元素都进行了编号(从0开始),因此,我们在操作并查集的时候,主要通过编号来进行,也就是数组的下标。
class UnionFindSet
{
public:
// 构造函数
UnionFindSet(size_t n)
:_ufs(n, -1)
{}
// 查找一个元素所在的集合
int FindRoot(int x);
// 判断两个元素是否在同一个集合中
bool InSet(int x1, int x2);
// 合并两个元素所在的集合
void Union(int x1, int x2);
// 获取集合的个数 注意:不是元素个数
size_t SetSize();
private:
vector<int> _ufs;
};
并查集中最重要的接口就是 合并两个集合Union 和 判断两个元素是否在同一个集合中InSet,这两个函数都需要借助 查找根FindRoot来进行,最后还有一个获取集合的个数SetSize。
构造函数
构造函数只需要将并查集中的所有元素值设置为-1即可,表示当前每个元素都是一个集合,集合中有一个元素。
class UnionFindSet
{
public:
// 构造函数:指定并查集的大小,将并查集中的元素值全部设置为-1
UnionFindSet(size_t n)
:_ufs(n, -1)
{}
private:
vector<int> _set;
};
查找元素属于哪个集合
对于并查集中的每个元素来说,我们需要标识元素属于哪个集合,我们采用集合中编号最小的元素为集合的根,