探索数据结构:并查集的分析与实现

news2024/11/25 10:55:27


✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog

1. 并查集的引入

1.1 并查集的概念

并查集是一种树型数据结构,主要用于处理不相交集合的合并及查询问题,在实际应用中常常以森林形式呈现。 在众多应用问题里,常常需要将n个不同元素划分成若干不相交的集合。初始状态下,每个元素各自构成一个单元素集合。随后,依据特定规律对归于同一组元素的集合进行合并操作。在此过程中,频繁涉及查询某一元素所属集合的运算。能够很好地描述这类问题的抽象数据结构即并查集。

1.2 并查集的特点

并查集的底层结构一般以数组表示,其一般具有以下三个特点:

  1. 数组的下标对应集合中元素的编号。
  2. 数组中元素如果为负数,代表这个下标是一颗树的根节点,数字的绝对值代表该集合中元素个数。
  3. 数组中原始如果为非负数,代表该元素父节点在数组中的下标。

比如说现在有个一个集合,其中分别有十个元素分别为{0,1,2,3,4,5,6,7,8,9}。现在它们之间毫无任何关系,所以每一个元素都可以单独看成一颗树。如果用数组表示,每个元素对应一个下标,每一个下标对应的值都为-1。

如果对该集合建立关系,比如说分为三个集合:{0,6,7,8}{1,4,9}{2,3,5}。这时我们就可以抽象为对应的三颗子树,对应数组也可更新。

如果我们再让元素0与元素1建立联系,如果这个集合就可合并为一颗子树,对应数组的值也需更新。

2. 并查集的功能

并查集具有常见的以下几个功能:

  1. 初始化并查集。
  2. 查找元素对应的根节点。
  3. 判断两个元素是否在同一个集合。
  4. 合并两个元素所在的集合。
  5. 获取并查集中集合的个数。

3. 并查集的功能实现

3.1 并查集的结构

并查集的结构实现非常简单,直接以数组作为底层结构即可。

//并查集
class UnionFindSet 
{
public:
	//构造函数
	UnionFindSet(int n);
	//查找根节点
	int findRoot(const int x);
	//判断两个元素是否在同一个集合
	bool inSameSet(const int x1, const int x2);
	//合并两个元素所在的集合
	bool unionSet(const int x1, const int x2);
	//获取并查集中集合的个数
	int getNum();
private:
	vector<int> _ufs; //数组实现
};

3.2 并查集的初始化

通过上面图示我们知道并查集初始化时每一个数据都是一个根节点,所以全部初始化为-1即可。

UnionFindSet(size_t n)
    :_ufs(n, -1)
{}

又因为并查集的成员变量都是自定义类型,所以不需要显示写对应的析构函数。

3.3 查找根节点

查找根节点只需要找到对应值为负数的下标即可。

int findRoot(int x)
{
    int root = x;
    //如果root大于0则不为根
    while (_ufs[root] >= 0)
    {
        root = _ufs[root];
    }
    return root;
}

当然我们也可以采用递归的方式实现。

//递归实现
int findRoot(int x) 
{
	return _ufs[x] < 0 ? x : findRoot(_ufs[x]);
}

3.4 合并两个集合

合并两个集合需要先找到对应的更节点,然后我们默认将小集合合并到大集合上,然后更新大集合的元素个数,以及小集合的根节点。

//合并两个集合
bool unionSet(const int x1, const int x2)
{
    int root1 = findRoot(x1);
    int root2 = findRoot(x2);
    if (root1 == root2)
    {
        return false;
    }
    //将小集合拼接到大集合上
    //让root1代表大集合,root2代表小集合
    if (_ufs[root1] > _ufs[root2])
    {
        swap(root1, root2);
    }
    //大集合的元素个数增加
    _ufs[root1] += _ufs[root2];
    //小集合的父节点改为大集合
    _ufs[root2] = root1;
    return true;
}

3.5 判断两个元素是否在同一个集合

我们只需找到两元素的根节点判断是否相同即可。

//判断两个元素是否在同一个集合
bool inSameSet(const int x1, const int x2)
{
    int root1 = findRoot(x1);
    int root2 = findRoot(x2);
    return root1 == root2;
}

3.6 获取并查集的集合个数

获取集合个数即判断有几个根节点。

//获取集合的个数
int getNum()
{
    int count = 0;
    //遍历数组如果小于0则为根
    for (auto& e : _ufs)
    {
        if (e < 0)
        {
            ++count;
        }
    }
    return count;
}

4. 并查集的优化

4.1 路径压缩

在我们不断将两个集合合并的过程中,可能会出现某种极端的情况,使查找对应的根节点的效率接近线性,大大降低我们的查找效率。为了避免发生这种情况,我们可以采用一种路径压缩的形式。

路径压缩简单来说就是把节点都与根节点直接相连,减少查找效率。

路径压缩一般我们都在查找中顺便实现。

int findRoot(int x)
{
    int root = x;
    //如果root大于0则不为根
    while (_ufs[root] >= 0)
    {
        root = _ufs[root];
    }
    //路径压缩
    while (_ufs[x] >= 0)
    {
        //记录父节点
        int parent = _ufs[x];
        //指向根节点
        _ufs[x] = root;
        x = parent;
    }
    return root;
}

当然我们也可以采用递归实现。

//递归查找
int findRoot(int x) 
{
	int root = x; 
	if (_ufs[x] >= 0) 
    { 
         root = findRoot(_ufs[x]); //找到根结点
		_ufs[x] = root; //进行路径压缩
	}
	return root; //返回根结点
}

4.2 泛型编程

我们可以利用模版实现一个针对不同类型的并查集,而为了方便不同类型映射方便,我们还需要增加一个成员变量来映射元素与下标之间的联系。

template<class T>
class UnionFindSet
{
public:
private:
	vector<int> _ufs;//并查集的底层为数组
	unordered_map<T, int> _indexMap;//建立元素与下标直接的练习
};

然后我们可以先初始化时映射数据与下标的关系,查找时可以直接通过数据找到下标。

UnionFindSet(const vector<T>& v)
    :_ufs(v.size(), -1)
{
    //每一个元素都映射一个下标
    for (int i = 0; i < v.size(); i++)
    {
        _indexMap[v[i]] = i;
    }
}
//查找根节点
int findRoot(const T& x)
{
    //找到对应下标
    int root = _indexMap[x];
    //如果root大于0则不为根
    while (_ufs[root] >= 0)
    {
        root = _ufs[root];
    }
    //路径压缩
    int index = _indexMap[x];
    while (_ufs[index] >= 0)
    {
        //记录父节点
        int parent = _ufs[index];
        //指向根节点
        _ufs[index] = root;
        index = parent;
    }
    return root;
}

5. 复杂度分析

以下是对上述并查集代码中各个操作的时间复杂度与空间复杂度分析:
二、时间复杂度分析

  1. 并查集的初始化:

整体时间复杂度为 O ( n ) O(n) O(n)

  • 初始化 _ufs 为大小 v.size() 的向量,时间复杂度为 O ( n ) O(n) O(n)
  • 通过遍历 v 建立 _indexMap,时间复杂度为 O ( n ) O(n) O(n),其中 n n n 是输入向量 v 的大小。
  1. 并查集的查找根节点操作:

整体时间复杂度接近 O ( 1 ) O(1) O(1)

  • 第一次查找对应下标的时间复杂度为 O ( 1 ) O(1) O(1),因为 unordered_map 的查找操作平均时间复杂度为 O ( 1 ) O(1) O(1)
  • 路径压缩过程中,最坏情况下需要遍历整个集合,时间复杂度为 O ( l o g n ) O(log n) O(logn),其中 n n n 是集合的大小,但由于路径压缩的效果,后续查找操作会更快,实际平均时间复杂度接近 O ( 1 ) O(1) O(1)
  1. 并查集合并两个集合操作:

整体时间复杂度接近 O ( 1 ) O(1) O(1)

  • 查找两个元素的根节点,时间复杂度接近 O ( 1 ) O(1) O(1)(因为调用了 findRoot)。
  • 合并操作时间复杂度为 O ( 1 ) O(1) O(1)
  1. 判断两个元素是否在同一个集合操作:

整体时间复杂度接近 O ( 1 ) O(1) O(1)

  • 查找两个元素的根节点,时间复杂度接近 O ( 1 ) O(1) O(1)(因为调用了 findRoot)。
  • 比较两个根节点是否相同,时间复杂度为 O ( 1 ) O(1) O(1)
  1. 获取并查集的集合个数操作:

整体时间复杂度为 O ( n ) O(n) O(n),但通常情况下 n 较大时这个操作执行次数较少。

  • 遍历 _ufs 向量,时间复杂度为 O ( n ) O(n) O(n),其中 n n n 是输入向量 v 的大小。

二、空间复杂度分析

  1. 并查集整体的空间复杂度取决于两个数据结构:
  • vector<int> _ufs:存储每个元素的父节点信息或集合大小(负数表示根节点且绝对值为集合大小)。其空间复杂度为 O ( n ) O(n) O(n),其中 n n n 是输入向量 v 的大小。
  • unordered_map<T, int> _indexMap:建立元素与下标之间的映射。其空间复杂度取决于输入元素的数量,最坏情况下为 O ( n ) O(n) O(n),其中 n n n 是输入向量 v 的大小。

所以,整体空间复杂度为 O ( n ) O(n) O(n),其中 n n n 是输入向量 v 的大小。

综上所述,并查集的大部分操作时间复杂度接近 O ( 1 ) O(1) O(1),空间复杂度为 O ( n ) O(n) O(n)

6. 源码

template<class T>
class UnionFindSet
{
public:
	UnionFindSet(const vector<T>& v)
		:_ufs(v.size(), -1)
	{
		//每一个元素都映射一个下标
		for (int i = 0; i < v.size(); i++)
		{
			_indexMap[v[i]] = i;
		}
	}
	//查找根节点
	int findRoot(const T& x)
	{
		//找到对应下标
		int root = _indexMap[x];
		//如果root大于0则不为根
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}
		//路径压缩
		int index = _indexMap[x];
		while (_ufs[index] >= 0)
		{
			//记录父节点
			int parent = _ufs[index];
			//指向根节点
			_ufs[index] = root;
			index = parent;
		}
		return root;
	}
	//合并两个合集
	bool unionSet(const T& x1, const T& x2)
	{
		int root1 = findRoot(x1);
		int root2 = findRoot(x2);
		if (root1 == root2)
		{
			return false;
		}
		//将小集合拼接到大集合上
		//让root1代表大集合,root2代表小集合
		if (_ufs[root1] > _ufs[root2])
		{
			swap(root1, root2);
		}
		//大集合的元素个数增加
		_ufs[root1] += _ufs[root2];
		//小集合的父节点改为大集合
		_ufs[root2] = root1;
		return true;
	}
	//判断两个元素是否在同一个集合
	bool inSameSet(const int x1, const int x2)
	{
		int root1 = findRoot(x1);
		int root2 = findRoot(x2);
		return root1 == root2;
	}
	//获取并查集的个数
	int getNum()
	{
		int count = 0;
		//遍历数组如果小于0则为根
		for (auto& e : _ufs)
		{
			if (e < 0)
			{
				++count;
			}
		}
		return count;
	}
private:
	vector<int> _ufs;//并查集的底层为数组
	unordered_map<T, int> _indexMap;//建立元素与下标直接的练习
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2061429.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

StarRocks 存算分离数据回收原理

前言 StarRocks存算分离表中&#xff0c;垃圾回收是为了删除那些无用的历史版本数据&#xff0c;从而节约存储空间。考虑到对象存储按照存储容量收费&#xff0c;因此&#xff0c;节约存储空间对于降本增效尤为必要。 在系统运行过程中&#xff0c;有以下几种情况可能会需要删…

详解华为项目管理,附华为高级项目管理内训材料

&#xff08;一&#xff09;华为在项目管理中通过有效的沟通、灵活的组织结构、坚持不懈的努力、细致的管理和科学的考核体系&#xff0c;实现了持续的创新和发展。通过引进先进的管理模式&#xff0c;强调以客户需求为导向&#xff0c;华为不仅优化了技术管理和项目研发流程&a…

el-table自定义样式,表头固定,数据过多时滚动

最终效果&#xff1a;&#xff08;此处没体现出来滚动&#xff0c;数据没那么多&#xff09; 1.表头固定&#xff0c;设置表头样式&#xff0c;修改表格背景色 <div class"category-table"> <el-table ref"tableRef" class"common-table&quo…

java之类和对象的介绍

1.面向对象和面向过程的概念&#xff1a; 面向对象&#xff1a;面向对象是解决问题的一种思想&#xff0c;主要依靠对象之间的交互完成一件事。 面向过程&#xff1a;注重完成一件事情的过程&#xff0c;后续代码维护扩展较为麻烦。 以洗衣服为例&#xff0c;面向对象为传统…

微软AI人工智能认证有哪些?

微软提供的人工智能认证主要包括以下几个方面&#xff1a; Azure AI Fundamentals&#xff08;AI900认证&#xff09;&#xff1a;这是一个基础认证&#xff0c;旨在展示与Microsoft Azure软件和服务开发相关的基本AI概念&#xff0c;以创建AI解决方案。它面向具有技术和非技术…

C++学习路线分享

我上大学学的第一门编程语言便是C&#xff0c;靠着那本饱受诟病的谭浩强版的教材度过了大一上学期。学的内容现在看来相当之浅&#xff0c;如果没记错的话只学了个基本的语法&#xff0c;考试的时候考一些冒泡&#xff0c;快排之类的东西就结束了。感觉那些有计算机教育背景的学…

解决Qt多线程中fromRawData函数生成的QByteArray数据不一致问题

解决Qt多线程中fromRawData函数生成的QByteArray数据不一致问题 目录 &#x1f514; 问题背景&#x1f4c4; 问题代码❓ 问题描述&#x1fa7a; 问题分析✔ 解决方案 &#x1f514; 问题背景 在开发一个使用Qt框架的多线程应用程序时&#xff0c;我们遇到了一个棘手的问题&…

【Linux】生产消费模型实践 --- 基于信号量的环形队列

你送出去的每颗糖都去了该去的地方&#xff0c; 其实地球是圆的&#xff0c; 你做的好事终会回到你身上。 --- 何炅 --- 基于信号量的环形队列 1 信号量2 框架构建3 代码实现4 测试运行 1 信号量 信号量本质是一个计数器&#xff0c;可以在初始化时对设置资源数量&#xf…

数据结构——链式队列和循环队列

目录 引言 队列的定义 队列的分类 1.单链表实现 2.数组实现 队列的功能 队列的声明 1.链式队列 2.循环队列 队列的功能实现 1.队列初始化 (1)链式队列 (2)循环队列 (3)复杂度分析 2.判断队列是否为空 (1)链式队列 (2)循环队列 (3)复杂度分析 3.判断队列是否…

91. UE5 RPG 实现拖拽装配技能以及解除委托的绑定

在上一篇文章里&#xff0c;实现了通过选中技能&#xff0c;然后点击下方的装备技能插槽实现了技能的装配。为了丰富技能装配功能&#xff0c;在这一篇里&#xff0c;我们实现一下通过拖拽技能&#xff0c;实现拖拽功能&#xff0c;我们需要修改两个用户控件&#xff0c;一个就…

鸿蒙内核源码分析(信号生产篇) | 注意结构体的名字和作用.

信号生产 关于信号篇&#xff0c;本只想写一篇&#xff0c;但发现把它想简单了&#xff0c;内容不多&#xff0c;难度极大.整理了好长时间&#xff0c;理解了为何<<深入理解linux内核>>要单独为它开一章&#xff0c;原因有二 信号相关的结构体多&#xff0c;而且…

RTC碰到LXTAL低频晶振停振怎么办?

GD32F303的RTC模块框图如下图所示&#xff0c;RTC时钟源可选择HXTAL/128、LXTAL或IRC40K&#xff0c;一般为了实现更精准的RTC时间&#xff0c;MCU系统均会外挂32.768KHz LXTAL低频晶振&#xff0c;但由于低频晶振负阻抗较大&#xff0c;不容易起振&#xff0c;若外部电路布线、…

vue3 antdv3 去掉Modal的阴影背景,将圆角边框改为直角的显示,看上去不要那么的立体的样式处理。

1、来个没有处理的效果图&#xff1a; 这个有立体的效果&#xff0c;有阴影的效果。 2、要处理一下样式&#xff0c;让这个阴影的效果去掉&#xff1a; 图片的效果不太明显&#xff0c;但是阴影效果确实没有了。 3、代码&#xff1a; /* 去掉遮罩层阴影 */.ant-modal-mask {…

Maven命令传pom或者jar异常

上传命令&#xff1a;mvn deploy:deploy-file -Durlhttp://****&#xff1a;****/repository/chntdrools7741-releases -DrepositoryId**** -DfileD:/tempRepo/org/kie/kie-api-parent/7.69.0.Final/kie-api-parent-7.69.0.Final.pom -DpomFileD:/tempRepo/org/kie/kie-api-par…

三级_网络技术_39_综合题(命令)

一、 如下图所示&#xff0c;某校园网用10Gbps 的POS技术与Internet相连&#xff0c;POS接网的幅格式早SDH。路由协议的选择方案是校园网内部采用OSPF协议&#xff0c;校园网与lntemnet的连接使用静态路由协议。校园网内的路由器R1设为DHCP服务器&#xff0c;可分配的IP地址是…

【22-54】创建者模式(详解五大模式)

目录 一.创建者模式介绍 二.单例设计模式 2.1 单例模式的结构 2.2 单例模式的实现 2.2.1.1 饿汉式-方式1&#xff08;静态变量方式&#xff09; 2.2.1.2 饿汉式-方式2&#xff08;静态代码块方式&#xff09; 2.2.2.1 懒汉式-方式1&#xff08;线程不安全&#xff09; 2…

用手机写一本电子书

第1步、进入Andi.cn网站 第2步、点击登录&#xff0c;注册用户 第3步、点击去创作&#xff0c;进入创作页面 第4步、点击右下角的小笔&#xff0c;写一篇文章 第5步、下翻&#xff0c;点击提交按钮 第6步、再写一篇文章 第7步、点击栏目设计 第8步、进入栏目设计&#xff0c;点…

excel卓越之道笔记

excel快捷键 1.Alt+=一键求和 2.Tab补全函数名称 3.CONCAT可以连选,CONCATENATE只能一个单元格一个单元格点选 4.excel365用不了phonetic函数,但是可以用concat代替 5.textjoin连接标识码,在Arcgis中筛选出所需要素,也是很好用的 6.法1:alt+; 定位可见单元格,复制后只…

Linux入门——01常用命令

0.命令行解释器shell 用户无法直接给操作系统指令&#xff0c;需要经过shell,才能让操作系统明白。如果用户对操作系统非法操作&#xff0c;会有shell保护。shell本身也是一个进程&#xff0c;当然&#xff0c;用户给shell的指令&#xff0c;shell会派生出子进程进行执行&#…

Unity Protobuf3.21.12 GC 问题(反序列化)

背景&#xff1a;Unity接入的是 Google Protobuf 3.21.12 版本&#xff0c;排查下来反序列化过程中的一些GC点&#xff0c;处理了几个严重的&#xff0c;网上也有一些分析&#xff0c;这里就不一一展开&#xff0c;默认读者已经略知一二了。 如果下面有任何问题请评论区留言提…