【数据结构】一篇文章了解并查集

32 篇文章 11 订阅
18 篇文章 7 订阅


前提:并查集的概念

并查集作为一种数据结构在处理需要将n个元素划分成不相交的集合,在逐步按照某一种规律将某些元素给合并,并在这个过程中要反复查询某一种元素归属于哪个集合的运算,称之为并查集


并查集的用途

剑指 Offer II 116. 朋友圈
在这里插入图片描述

这道题讲述就是有一组元素,他们相互之间产生某种关系,形成了集合,面对这种问题,常规的解法弄来弄去可能都会觉得做的不太对,但有了并查集我们解决这类问题是较简单的。


并查集的结构解释

注释:双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点

我们简要的举个例子,这里我们有5人,他们的编号为 0 - 4,假设他们是一群准大一的学生,他们刚开始各自为一个集合。
在这里插入图片描述
我们这里用负数表示自己为整体,索引指向的内容若为正数表示该索引存放的值为集合当中的双亲结点。

假设过了一段时间后,编号为0,2,4的同学形成了一个集合。编号为1,3的同学形成了一个集合。,如下图所示

在这里插入图片描述这里的0,1分别是一个集合,索引对应的值是集合中的元素个数的相反数

解释: 此时父节点中存放的是集合大小的负值,子节点存放的是父节点的索引,倘若这时编号为4 与编号为3的结点因为一场比赛结识成为了朋友,我们要如何做呢? - -实际上,当4与3结合成朋友,自然两个集合中的每一个人都成了朋友,我们这时只需要让其中一个集合成为父节点,两个集合便成了一个集合了 。

在这里插入图片描述

此时我们的人形成了一个整体,每个子节点的存放的都是父节点的下标(0),0位置存放结点的个数的负值(因为我们用负数来区分是否为根


我们也可以看出来一个并查集所需要具备哪些结论?
1.数组的下标对应集合中的元素下标
2.负数代表根,数字代表该集合中的元素个数
3.若数组中为非负数,则为父节点在数组当中的下标
我们也可以看出来一个并查集所需要具备哪些功能?
1.查找元素属于哪一个集合
2.查找两个元素是否为一个集合
3.两个元素合并成一个集合
4.集合的个数


并查集的实现

注释中有解释,详情请看注释,并结合下面的两道习题,其实就是实现了上面概括的四个接口

#include<iostream>
using namespace std;
#include<vector>
#include<assert.h>
class UnionFindSet
{
public:
//构造函数,初始化开空间
	UnionFindSet(int x)
	{
		_ufs.resize(x, -1);
		//初始化的时候讲x个空间初始化成-1
	}
	//合并两个元素为集合
	bool Union(int x1,int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2)
			return false;
		else
		{
			//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
			return true;
		}
	}
	int FindRoot(int x)
	{
		assert(x < _ufs.size());
		while (_ufs[x] >= 0)
		{
			x = _ufs[x];
		}
		return x;
	}
	//算出数组当中有多少个集合
	int Size()
	{
		int count = 0;
		for (int i = 0; i < _ufs.size(); ++i)
		{
		//计算数组中有几个集合
			if (_ufs[i] < 0)
				count++;
		}
		return count;
	}
private:
	vector<int> _ufs;
};

并查集相关习题

习题1.leetcode 朋友圈

朋友圈
在这里插入图片描述

分析: 这题其实就是给了我们一个二维矩阵,然后让我们确定有多少个朋友圈,这个就很简单了,我们可以遍历一遍数组,用我们的Union的接口将两个人合并成一个集合,最后在用Size接口来计算集合的个数。

class UnionFindSet
{
public:
	UnionFindSet(int x)
	{
		_ufs.resize(x, -1);
		//初始化的时候讲x个空间初始化成-1
	}
	bool Union(int x1,int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2)
			return false;
		else
		{
			//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
			return true;
		}
	}
	int FindRoot(int x)
	{
		assert(x < _ufs.size());
		while (_ufs[x] >= 0)
		{
			x = _ufs[x];
		}
		return x;
	}
	int Size()
	{
		int count = 0;
		for (int i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
				count++;
		}
		return count;
	}
private:
	vector<int> _ufs;
};
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
    UnionFindSet ufs(isConnected.size());
    for(int i =0;i<isConnected.size();++i)
    {
        for(int j =0;j<isConnected.size();++j)
        {
            //遍历二维数组,将 i==j自己与自己的情况过滤
            if(i == j)
            continue;
            //将为朋友的弄成集合
            if(isConnected[i][j]==1)
            ufs.Union(i,j);
        }
    }
    int ret = ufs.UfsSize();
    return ret;
    }
};

习题2.leetcode 等式方程的可满足性

等式方程的可满足性
在这里插入图片描述
在这里插入图片描述

分析:这道题目也是用并查集实现为最优解,我们这道题可以先遍历一遍数组,将所有string中的第二个位置为 ‘=’先放入并查集当中,在遍历一遍从第二个位置’!'查看是否有重复的错误逻辑放入

class UnionFindSet
{
public:
	UnionFindSet(int x)
	{
		_ufs.resize(x, -1);
		//初始化的时候讲x个空间初始化成-1
	}
	bool Union(int x1,int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2)
			return false;
		else
		{
			//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
			return true;
		}
	}
	int FindRoot(int x)
	{
		assert(x < _ufs.size());
		while (_ufs[x] >= 0)
		{
			x = _ufs[x];
		}
		return x;
	}
	int Size()
	{
		int count = 0;
		for (int i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
				count++;
		}
		return count;
	}
private:
	vector<int> _ufs;
};
class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        UnionFindSet ufs(26);
        //先遍历一遍,将所有==的情况的集合都找出来,再第二次遍历将!=看==的情况是否存在
        for(auto& e: equations)
        {
        //第一次遍历,建立映射
            if(e[1] =='=')
            ufs.Union(e[0]-'a',e[3]-'a');
        }
        for(auto& e:equations)
        {
            if(e[1] =='!')
            {
            //第二次遍历,查错
            //若他们的头结点为同一个,说明在一个集合,则与逻辑错误
                if(ufs.FindRoot(e[0]-'a') == ufs.FindRoot(e[3]-'a'))
                return false;
            }
        }
        //当走到这里的时候,表示并查集中没有出现逻辑错误的
        return true;
    }
};

并查集的思考与提升

这里大家思考一下,朋友圈这样的题型,倘若他给我们的不是编号,而是人名,那么我们如何利用我们的并查集呢,我们的并查集都是通过下标来实现的。
在这里插入图片描述
我们这里可以用vector< string > nameV={“小明”,“小红”,“小绿”…},这样子我们就可以通过下标找到对应的人。
我们用人找下标就可以用map<string,int> nameIndexmap 来通过人名找到对应下标,这样我们的并查集就可以实现通用了

通用分析:
map 可以由名字找到下标

_ufs[下标] 可以找到对应的头结点

vector 可以由下标找到人

我们想要进行人名对应查找头节点的时候,map(人名)首先可以找到下标,vector[下标]可以找到对应的双亲结点的人,相当于找到了对应的双亲结点的名字,我们就可以利用这种结构对_ufs这个数组进行更改


总结

  • 喜欢就收藏
  • 认同就点赞
  • 支持就关注
  • 疑问就评论
  • 15
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
网管教程 从入门到精通软件篇 ★一。★详细的xp修复控制台命令和用法!!! 放入xp(2000)的光盘,安装时候选R,修复! Windows XP(包括 Windows 2000)的控制台命令是在系统出现一些意外情况下的一种非常有效的诊断和测试以及恢复系统功能的工具。小编的确一直都想把这方面的命令做个总结,这次辛苦老范给我们整理了这份实用的秘笈。   Bootcfg   bootcfg 命令启动配置和故障恢复(对于大多数计算机,即 boot.ini 文件)。   含有下列参数的 bootcfg 命令仅在使用故障恢复控制台时才可用。可在命令提示符下使用带有不同参数的 bootcfg 命令。   用法:   bootcfg /default  设置默认引导项。   bootcfg /add    向引导列表中添加 Windows 安装。   bootcfg /rebuild  重复全部 Windows 安装过程并允许用户选择要添加的内容。   注意:使用 bootcfg /rebuild 之前,应先通过 bootcfg /copy 命令备份 boot.ini 文件。   bootcfg /scan    扫描用于 Windows 安装的所有磁盘并显示结果。   注意:这些结果被静态存储,并用于本次会话。如果在本次会话期间磁盘配置发生变化,为获得更新的扫描,必须先重新启动计算机,然后再次扫描磁盘。   bootcfg /list   列出引导列表中已有的条目。   bootcfg /disableredirect 在启动引导程序中禁用重定向。   bootcfg /redirect [ PortBaudRrate] |[ useBiosSettings]   在启动引导程序中通过指定配置启用重定向。   范例: bootcfg /redirect com1 115200 bootcfg /redirect useBiosSettings   hkdsk   创建并显示磁盘的状态报告。Chkdsk 命令还可列出并纠正磁盘上的错误。   含有下列参数的 chkdsk 命令仅在使用故障恢复控制台时才可用。可在命令提示符下使用带有不同参数的 chkdsk 命令。   vol [drive:] [ chkdsk [drive:] [/p] [/r]   参数  无   如果不带任何参数,chkdsk 将显示当前驱动器中的磁盘状态。 drive: 指定要 chkdsk 检查的驱动器。 /p   即使驱动器不在 chkdsk 的检查范围内,也执行彻底检查。该参数不对驱动器做任何更改。 /r   找到坏扇区并恢复可读取的信息。隐含着 /p 参数。   注意 Chkdsk 命令需要 Autochk.exe 文件。如果不能在启动目录(默认为 %systemroot%System32)中找到该文件,将试着在 Windows 安装 CD 中找到它。如果有多引导系统的计算机,必须保证是在包含 Windows 的驱动器上使用该命令。 Diskpart   创建和删除硬盘驱动器上的分区。diskpart 命令仅在使用故障恢复控制台时才可用。   diskpart [ /add |/delete] [device_name |drive_name |partition_name] [size]   参数 无   如果不带任何参数,diskpart 命令将启动 diskpart 的 Windows 字符模式版本。   /add   创建新的分区。   /delete   删除现有分区。   device_name   要创建或删除分区的设备。设备名称可从 map 命令的输出获得。例如,设备名称:   DeviceHardDisk0   drive_name   以驱动器号表示的待删除分区。仅与 /delete 同时使用。以下是驱动器名称的范例:   D:   partition_name   以分区名称表示的待删除分区。可代替 drive_name 使用。仅与 /delete 同时使用。以下是分区名称的范例:   DeviceHardDisk0Partition1    大小   要创建的分区大小,以兆字节 (MB)表示。仅与 /add 同时使用。   范例   下例将删除分区: diskpart /delete Device HardDisk0 Partition3 diskpart /delete F:   下例将在硬盘上添加一个 20 MB 的分区:   diskpart /add Device HardDisk0 20   Fixboot

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

^jhao^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值