⭐️ 今天要和大家介绍一个新的数据结构——并查集。听名字好像是把集合合并再查找元素,其实总体来说也是这样的,下面我们来和大家好好聊一聊这个玩意~
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/data-structure
并查集: 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集其实就是一个森林,由多个集合合并而成。
举例说明:
假设有10个人,给他们编号,依次是0-9,他们之间互相不认识,也就是说每个人都是一个独立的集合,用并查集表示就是如下图所示(其中每个成员是下标,内容为负数,代表该集合元素个数是该负数的绝对值,为正数,代表该下标的双亲是该正数。例如:对应的内容就是-1就是该小集体中元素个数为1,也就是自己一个元素):
现在,编号为0,3,4这三个人结识称为朋友,构成了一个朋友圈,也就是三个人形成一个集合,合并过程如下图:
下面是集合的树形表示:
接下来,编号为2,5,7,8成为一个朋友圈,编号为1,6,9成为一个朋友圈,合并结果如下:
用树形结构表示如下:
最后再操作一次,如果3和6结识称为好朋友,那么这两个朋友圈就要合并。合并分为两个步骤:
总结:
总体来说,实现起来还是比较简单的,底层我们选择用vector来实现,构造函数将vector的内容都初始化为-1,框架如下:
class UnionFindSet
{
public:
UnionFindSet(size_t size)
:_ufs(size, -1)
{}
private:
vector<int> _ufs;
};
步骤:
代码实现如下:
int FindRoot(int x)
{
assert(x >= 0 && x < _ufs.size());
int index = x;
while (_ufs[index] >= 0)
{
// 更新双亲
index = _ufs[index];
}
return index;
}
两个步骤:
void Union(int x1, int x2)
{
assert(x1 >= 0 && x1 < _ufs.size());
assert(x2 >= 0 && x2 < _ufs.size());
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
// 双亲不同就合并,否则不做处理
if (root1 != root2)
{
_ufs[root1] += _ufs[root2];// 把root2合并到root1
_ufs[root2] = root1;// 改变祖先
}
}
很简单,遍历一遍数组,统计内容为负数的下标的个数,即统计了集合个数
代码实现如下:
int Size()
{
int count = 0;
for (auto e : _ufs)
{
if (e < 0)
++count;
}
return count;
}
这里以LeetCode中的一道题为例,链接——省份数量
题目描述:
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
实例:
思路:
这里就是用到并查集的知识,我们可以通过遍历二维数组得到每两个城市的关系,因为isConnected[i][j]和isConnected[j][i]的内容其实是一样的,所以我们这里只需要遍历右上角的一部分内容即可确定所有的两个城市的关系。
如果两个城市有关系,那么就合并,没有就不合并,最后返回集合个数就是身份数。
解题代码如下:
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
// if (isConnected.size() == 1) return 1;
vector<int> ufs(isConnected.size(), -1);
for (size_t i = 0; i < isConnected.size(); ++i)
{
for (size_t j = i + 1; j < isConnected.size(); ++j)
{
// 为1j就合并
if (isConnected[i][j]) Union(ufs, i, j);
}
}
int count = 0;
for (auto& e : ufs)
{
if (e < 0) count++;
}
return count;
}
int FIndRoot(vector<int>& ufs, int x)
{
int index = x;
while (ufs[index] >= 0)
{
index = ufs[index];
}
return index;
}
// 合并两个集合
void Union(vector<int>& ufs, int x1, int x2)
{
int root1 = FIndRoot(ufs, x1);
int root2 = FIndRoot(ufs, x2);
// 祖先不同就合并
if (root1 != root2)
{
ufs[root1] += ufs[root2];// 把root2合并到root1
ufs[root2] = root1;// 改变祖先
}
}
};
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。应用的地方也比较多,查找的时间复杂度是每个集合的深度次,效率也很不错。今天的内容就到这里了,喜欢的话,欢迎点赞支持和关注~