【数据结构高阶】第十一篇——并查集(原理+实现+应用)

⭐️ 今天要和大家介绍一个新的数据结构——并查集。听名字好像是把集合合并再查找元素,其实总体来说也是这样的,下面我们来和大家好好聊一聊这个玩意~
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/data-structure

目录

  • 概念和原理
  • 实现
    • 整体框架
    • 查找元素属于哪个集合
    • 合并两个集合
    • 统计集合个数
  • 应用
  • 总结


概念和原理

并查集: 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集其实就是一个森林,由多个集合合并而成。

举例说明:
假设有10个人,给他们编号,依次是0-9,他们之间互相不认识,也就是说每个人都是一个独立的集合,用并查集表示就是如下图所示(其中每个成员是下标,内容为负数,代表该集合元素个数是该负数的绝对值,为正数,代表该下标的双亲是该正数。例如:对应的内容就是-1就是该小集体中元素个数为1,也就是自己一个元素):
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第1张图片
现在,编号为0,3,4这三个人结识称为朋友,构成了一个朋友圈,也就是三个人形成一个集合,合并过程如下图:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第2张图片

下面是集合的树形表示:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第3张图片
接下来,编号为2,5,7,8成为一个朋友圈,编号为1,6,9成为一个朋友圈,合并结果如下:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第4张图片
用树形结构表示如下:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第5张图片
最后再操作一次,如果3和6结识称为好朋友,那么这两个朋友圈就要合并。合并分为两个步骤:

  1. 分别找到自己所在集合的根
  2. 把两个根按照上面的方法合并

树形图形式演示:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第6张图片
数组形式演示:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第7张图片

总结:

  1. 数组的下标对应集合中元素的编号
  2. 数组中如果为负数,负号代表根,数字的绝对值代表该集合中元素个数
  3. 数组中如果为非负数,代表该元素双亲在数组中的下标

实现

整体框架

总体来说,实现起来还是比较简单的,底层我们选择用vector来实现,构造函数将vector的内容都初始化为-1,框架如下:

class UnionFindSet
{
public:
	UnionFindSet(size_t size)
		:_ufs(size, -1)
	{}
private:
	vector<int> _ufs;
};

查找元素属于哪个集合

步骤:

  1. 当元素对应的下标对应的内容为正数时(也就是该元素不为根),我们需要更新该下标,也就是index=_ufs[index],让自己成为双亲
  2. 继续判断,如果对应内容为负数,说明该下标是根,直接返回

代码实现如下:

int FindRoot(int x)
{
	assert(x >= 0 && x < _ufs.size());

	int index = x;
	while (_ufs[index] >= 0)
	{
		// 更新双亲
		index = _ufs[index];
	}
	return index;
}

合并两个集合

两个步骤:

  1. 先分别找到两个集合对应的根
  2. 然后合并两个根,让其中一个根归并到一个根的下面,更新新的双亲的内容和被并入的根的内容(把内容修改为新根的下标)
    具体演示:
    【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第8张图片
    代码实现:
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 表示二者不直接相连。
返回矩阵中 省份 的数量。

实例:
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第9张图片
思路:
这里就是用到并查集的知识,我们可以通过遍历二维数组得到每两个城市的关系,因为isConnected[i][j]和isConnected[j][i]的内容其实是一样的,所以我们这里只需要遍历右上角的一部分内容即可确定所有的两个城市的关系。
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第10张图片

如果两个城市有关系,那么就合并,没有就不合并,最后返回集合个数就是身份数。
解题代码如下:

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)的合并及查询问题。常常在使用中以森林来表示。应用的地方也比较多,查找的时间复杂度是每个集合的深度次,效率也很不错。今天的内容就到这里了,喜欢的话,欢迎点赞支持和关注~
【数据结构高阶】第十一篇——并查集(原理+实现+应用)_第11张图片

你可能感兴趣的:(数据结构与算法,并查集)