c++ 红黑树(自平衡二叉搜索树)

news2024/9/25 9:29:56

目录

 红黑树的概念

红黑树的由来

红黑树的性质

红黑树结点的定义

红黑树的插入

情况一:插入结点的叔叔存在,且叔叔的颜色是红色。

情况二:插入结点的叔叔存在且颜色是黑色 / 叔叔不存在,

情况A:p为g的左孩子,cur为p的左孩子

情况B:p为g的右孩子,cur为p的右孩子

情况C:p为g的左孩子,cur为p的右孩子

情况D:p为g的右孩子,cur为p的左孩子

红黑树的验证

同样红黑树作为特别的AVL树也是禁止进行修改的!!!


 红黑树的概念

对于题目也可以大致猜出红黑树是一种什么类型的树,其是一种特殊的平衡二叉搜索树(AVL树)。其产生也是因为避免了AVL树中的单枝树的存在。从而整体上进行了优化。

之所以叫红黑树是因为:在每个结点上增加了一个存储位用于表示结点的颜色,这个颜色可以是红色的,也可以是黑色的,因此我们称之为红黑树。

红黑树的由来

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

红黑树的性质

红黑树有以下五点性质:

  1. 每个结点不是红色就是黑色。
  2. 根结点是黑色的。
  3. 如果一个结点是红色的,则它的两个孩子结点是黑色的。
  4. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。
  5. 每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。

由此还是可以引出一些疑惑的

比如:

红黑树如何确保从根到叶子的最长可能路径不会超过最短可能路径的两倍? 

 其实从性质三我们可以得到一个信息:红黑树当中不会出现连续的红色结点,所以父子节点只有三种颜色情况:红--->黑,黑--->黑,黑--->红。而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的。

我们假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色结点构成的路径,即长度为N。

与之对应的最长可能路径就是由一黑一红结点构成的路径,该路径当中黑色结点与红色结点的数目相同,即长度为2N。

因此,红黑树从根到叶子的最长可能路径不会超过最短可能路径的两倍。

这里再解释一下性质5,这里的叶子结点不是传统意义上的叶子,不是下图中的25/74/78/86/90

 

实际上,在红黑树中真正被定义为叶子结点的,是那些空节点,如下图。

 

 

红黑树结点的定义

因为红黑树是一种特殊的AVL树(但少了平衡因子的存在),所以其结点的定义是在AVL树上加上新的成员变量,用于表示结点的颜色。

enum Colour
{
	RED,
	BLACK,
};

template<class K, class V>
struct RBTreeNode
{
    //三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

首先我们在默认构造上,默认构造结点的颜色默认情况下为红色

所以为什么构造结点时,默认将结点的颜色设置为红色?

这是因为:当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。

若我们插入红黑树的结点是红色的,此时如果其父结点也是红色的,那么表明出现了连续的红色结点,即破坏了红黑树的性质3,此时我们需要对红黑树进行调整;但如果其父结点是黑色的,那我们就无需对红黑树进行调整,插入后仍满足红黑树的要求。

总的来说:

  • 插入黑色结点,一定破坏红黑树的性质4,必须对红黑树进行调整。
  • 插入红色结点,可能破坏红黑树的性质3,可能对红黑树进行调整。

权衡利弊后,我们在构造结点进行插入时,默认将结点的颜色设置为红色。

红黑树的插入

红黑树插入结点的逻辑分为三步:

  1. 按二叉搜索树的插入方法,找到待插入位置。
  2. 将待插入结点插入到树中。
  3. 若插入结点的父结点是红色的,则需要对红黑树进行调整。

看起来,其实插入的步骤其实与AVL树是大差不差的,前两步都是按照二叉搜索树的方式进行插入,与之不同的就是第三步,AVL树对此的解决方法就是进行旋转,红黑树作为特殊的AVL树,那么也是要进行旋转的,同样也设计到了旋转。

红黑树在插入结点后是否要进行旋转处理呢? 

实际上,在插入结点后并不是一定会对红黑树进行调整,若插入结点的父结点是黑色的,那么我们就不用对红黑树进行调整,因为本次结点的插入并没有破坏红黑树的五点性质。

所以要调整的大前提就是:只有当插入结点的父结点是红色时才需要对红黑树进行调整,因为我们默认插入的结点就是红色的,如果插入结点的父结点也是红色的,那么此时就出现了连续的红色结点,因此需要对红黑树进行调整。

因为插入结点的父结点是红色的,说明父结点不是根结点(根结点是黑色的),因此插入结点的祖父结点(父结点的父结点)就一定存在。

红黑树调整时具体应该如何调整,主要是看插入结点的叔叔(插入结点的父结点的兄弟结点),根据插入结点叔叔的不同,可将红黑树的调整总分为两种情况。

情况一:插入结点的叔叔存在,且叔叔的颜色是红色。

此时为了避免出现连续的红色结点,我们可以将父结点变黑,但为了保持每条路径黑色结点的数目不变,因此我们还需要将祖父结点变红,再将叔叔变黑。这样一来既保持了每条路径黑色结点的数目不变,也解决了连续红色结点的问题。 

但是这次调整还没有完全结束

  • 此时祖父结点变成了红色,如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。
  • 如果祖父不是根节点的话,因为祖父原本是黑色,所以其父亲可以是红色,也可以是黑色,但如果是红色的情况下,在本次的颜色调整过后,祖父变为了红,但其父亲又为红,这就会破坏性质三,这就还需要再看其祖父的叔叔的情况,再次进行选择调整。

因此,情况一的抽象图表示如下: 

注意: 叔叔存在且为红时,cur结点是parent的左孩子还是右孩子,调整方法都是一样的。

情况二:插入结点的叔叔存在且颜色是黑色 / 叔叔不存在,

对于这种情况的错误,肯定不是因为cur作为新增结点造成的错误,即使删掉cur那也是需要调整的,那么其造成的原因一定是因为在情况一继续往上调整的过程中出现的,所以对于此cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点。

所以对于此图需要进行修改

对于此的解决方案处理起来比较简单,但也细分为四种小情况,主要是cur与p,p与g的父子关系。

情况A:p为g的左孩子,cur为p的左孩子

解决方案:对g进行右单旋,p变为黑色,g变为红色。

情况B:p为g的右孩子,cur为p的右孩子

解决方案:对g进行左单旋,p变为黑色,g变为红色。

情况C:p为g的左孩子,cur为p的右孩子

解决方案:对p进行左单旋,后变为情况A(注意记得更换cur与p的名称,图中为了直观看,就不更换了)

情况D:p为g的右孩子,cur为p的左孩子

解决方案:对p进行右单旋,后变为情况B(注意记得更换cur与p的名称,图中为了直观看,就不更换了)

这里就拿情况A来说一些小点吧,我们从上面的分析,cur不是作为新插入的结点,而是因为情况一的调整后使得情况一下的祖父颜色变为了红,而导致情况二中的cur变为了红,所以我们得知a,b子树中至少是存在一个黑色结点的,所以再进行此次的调整后从抽象图中看貌似是不符合性质四,其实是符合的,然而更不必说c子树了,因为在本次的插入是原本就是一个完整的红黑树,c子树中必有一个黑结点,并且c中本就不会存在情况一,也不会导致情况二。

从思路上来讲是简便的,但是代码实现上还是非常复杂的

这里先直接展示代码,稍后说一下注意点

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);

			_root->_col = BLACK;
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;

		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		//默认结点颜色为红色

		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//大前提
			//parent在左
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//Node* uncle = parent->_right;//错误二
				if (uncle && uncle->_col == RED)
				{
				//情景一:cur->红,parent->红,grandfather->黑,uncle存在且为红
					//     g
					//   p   u
					// c
					// 
					//解决:p,u改为黑,g改为红,最后g为红所以,要继续向上调整
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{					
					if (cur == parent->_left)
					{
					//情景二:cur->红,parent->红,grandfather->黑,uncle不存在/为黑
					    //     g
					    //   p   u
					    // c
					    // 
					// 解决:对g右单旋,p改为黑,g改为红,最后g为黑所以,直接break
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
					//情景三:cur->红,parent->红,grandfather->黑,uncle不存在/为黑
					    //       g
				        //   p      u
					    //     c
				    // 解决:对p左单旋,后变为情景二。
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}				
					break;
				}
			}
			else//情景大概反着来
			{
				//1  uncle
				Node* uncle = grandfather->_left;//错误一
				//Node* uncle = parent->_right;//错误一
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}

				else
				{
					if (cur == parent->_right)//2
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//3
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}			
		}
		//最后
		_root->_col = BLACK;

		return true;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		subR->_left = parent;

		Node* parentParent = parent->_parent;

		parent->_parent = subR;
		if (subRL)
			subRL->_parent = parent;

		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
			{
				parentParent->_left = subR;
			}
			else
			{
				parentParent->_right = subR;
			}

			subR->_parent = parentParent;
		}
	}

	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		Node* parentParent = parent->_parent;

		subL->_right = parent;
		parent->_parent = subL;

		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parentParent->_left == parent)
			{
				parentParent->_left = subL;
			}
			else
			{
				parentParent->_right = subL;
			}

			subL->_parent = parentParent;
		}
	}

注意点:其实对于情况二中的A,B小情况其实进行的是单旋,而C,D小情况是进行的双旋,所以也可以将C,D单独归为情况三,就如上面的代码展示一样,然而对于每次的插入后,都需要进行检查是否破坏红黑树的结构,然后破坏的大前提就是其父亲结点也是红,然而对于情况一,我们在调整完后会将祖父颜色置为红,这就导致其可以继续向上进行查看是否导致发生情况二,三的存在,所以还需要继续向上,然而对于情况二,三都是调整完就已经保证了是完整的红黑树了,这就可以直接break跳出即可。

注意: 在红黑树调整后,需要将根结点的颜色变为黑色,因为红黑树的根结点可能在情况一的调整过程中被变成了红色。

红黑树的验证

红黑树也是一种特殊的二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质。

判断依据:

  1. 是否存在 红-红 
  2. 每条路径黑色结点是否相同个数
  3. 最长的不超过最短的二倍
  4. 根,叶子为黑
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	bool Check(Node* root, int blacknum, const int refVal)
	{
		if (root == nullptr)
		{
			if (refVal != blacknum)
			{
				cout << "存在黑色节点数量不相等的路径" << endl;
				return false;
			}
			return true;
		}
		if (root->_col == RED)
		{
			if (root->_parent->_col == RED)
			{
				cout << "有连续的红色节点" << endl;
				return false;
			}
		}
		if (root->_col == BLACK)
		{
			++blacknum;
		}
		return Check(root->_left, blacknum, refVal)
			&& Check(root->_right, blacknum, refVal);

	}
	bool IsBalance()
	{
		 //1:是否存在 红-红 
		//每条路径黑色结点是否相同个数
		//最长的不超过最短的二倍
		//根,叶子为黑
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		int refVal = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refVal;
			}
			cur = cur->_left;
		}
		int blacknum = 0;
		return Check(_root, blacknum, refVal);
	}

剩下的内容也就是像二叉搜索树的平常函数:查找,看看有多少层,打印某一层......这里不在多多展示

同样红黑树作为特别的AVL树也是禁止进行修改的!!!

红黑树的删除代码复杂,并且不会就不多说了,但也推荐篇好的文章:

【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树-CSDN博客

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

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

相关文章

Android 使用原生相机Camera在预览界面进行识别二维码或者图片处理

1 项目需求 最近项目中有个需求:使用原生相机在预览界面进行识别二维码和图片处理。其实这个需求不是很难,难在对预览画面的处理过程。 自己针对这个需求写了一个工具类,便于后续进行复盘,同时也分享给有类似需求的伙伴们。 2 遇到的问题 2.1 二维码识别成功率低 使用…

python网络爬虫(零)——认识网页结构

网页一般有三部分组成&#xff0c;分别是HTML&#xff08;超文本标记语言&#xff09;、CSS&#xff08;层叠样式表&#xff09;、JScript&#xff08;活动脚本语言&#xff09; 1.HTML HTML是整个网页的结构&#xff0c;相当于整个网站的框架。带“<”“>”符号都属于H…

Linux Grep案例

目录 一. 查询两个文件第一列的数据并去重二. 抽取日志中指定的字段三. 服务器指定时间点异常查询四. 从csv文件中抽取指定的数据五. 获取除了空白行和注释之外的部分 一. 查询两个文件第一列的数据并去重 &#x1f4da;file1.log 123 aaa 你好 345 bbb 我好 345 ccc 大家好 …

【最新华为OD机试E卷】空栈压数(200分)-多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-E/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,…

2024上海初中生古诗文大会备考:单选题真题和每道题独家解析

新的学年已经开始了&#xff0c;距离2024年初中生古诗文大会初选的线上自由报名选拔还有2个月&#xff08;官宣11月3日线上初选正式开赛&#xff09;&#xff0c;据好真题了解&#xff0c;有一些学校的老师已经开始准备校内选拔了&#xff0c;以古诗文大会作为一个抓手&#xf…

torch、torchvision、torchtext版本兼容问题

1、torch与torchtext版本兼容 参考torchtext PyPI 2、 torch与torchvision版本兼容 参考torchvision PyPI

喜羊羊做Python真题

以下内容&#xff0c;皆为原创&#xff0c;制作实属不易&#xff0c;感谢大家的关注和点赞。 一.全局变量和局部变量 首先&#xff0c;全局变量是ls和lt。我们可以看到&#xff0c;函数内部lt列表的值 赋值 给了ls的列表。在定义的函数里面&#xff0c;ls是局部变量&#xff…

云同步的使用

云同步技术是一种在多个设备或系统之间保持数据一致性的技术&#xff0c;它通常依赖于云存储服务来实现。在Java中&#xff0c;实现云同步功能通常需要与云服务提供商的API进行交互&#xff0c;如Amazon S3、Google Cloud Storage、Microsoft Azure Blob Storage等。 以下是一个…

报错:java:程序包org.springframework.boot不存在

Date: 2024.08.31 18:01:20 author: lijianzhan 简述&#xff1a;关于java:程序包org.springframework.boot不存在问题如何进行修复。 操作如下&#xff1a; 点击左侧菜单栏选择设置&#xff0c;弹框内选择构建,执行&#xff0c;部署----->构建工具点击Maven按键&#xf…

python实战三-提取Word数据到Excel

视频源码&#xff1a; 链接&#xff1a;https://pan.quark.cn/s/83db5bb15383 一个文件夹下有大量会议通知文件&#xff0c;为word文件&#xff0c;文件格式都是一致的&#xff0c;现在要将文件中的一些字段提取出来汇总到Excel文件中。 会议通知文件格式如下&#xff1a; 要提…

Arthas线上诊断神器-如何安装?

文章目录 一、快速安装 ✅二、springboot-启动器 ✅ 一、快速安装 ✅ 第一种方式&#xff1a;通过下载arthas-boot.jar&#xff0c;然后用 java -jar 的方式启动&#xff5e; 第一步、下载arthas-boot.jar curl -O https://arthas.aliyun.com/arthas-boot.jar如果下载速度比较…

Linux镜像文件调整分区大小

本文介绍Linux镜像文件调整分区大小。 嵌入式系统在安装完Linux系统&#xff0c;磁盘通常默认只划分了2个分区&#xff0c;一个启动分区&#xff08;boot分区&#xff09;&#xff0c;一个根文件系统分区&#xff08;rootfs分区&#xff09;。有时&#xff0c;我们想划分一个独…

java打包混淆插件proguard

java代码混淆工具ProGuard混淆插件&#xff1a;https://blog.csdn.net/yinjl123/article/details/138922335 ProGuard Maven Plugin 使用指南&#xff1a;https://blog.csdn.net/gitblog_00427/article/details/141350545 ProGuard Maven Plugin 踩坑记 ProGuard代码混淆器如何…

火爆全网的扩散模型(Diffusion Model)到底是什么?只看这篇就够了!绝对通俗易懂!草履虫看完都要点头!| 附完整代码 + 详细注释

最近在做蛋白从头设计相关研究&#xff0c;看了超多文献&#xff08;后面也想慢慢把它们分享出来嘿嘿嘿&#xff09;&#xff01;发现现在最新的模型其实多数都是基于扩散模型&#xff08;Diffusion Model&#xff09;&#xff0c;既然如此&#xff0c;依据咱们要知道的尿性&am…

【Python进阶】学习Python必须要安装PyCharm。一篇文章带你总结一下安装PyCharm的注意事项,文末附带PyCharm激活码!!!

PyCharm激活码&#xff08;文末附带精品籽料&#xff09;&#xff1a; K384HW36OB-eyJsaWNlbnNlSWQiOiJLMzg0SFczNk9CIiwibGljZW5zZWVOYW1lIjoibWFvIHplZG9uZyIsImxpY2Vuc2VlVHlwZSI6IlBFUlNPTkFMIiwiYXNzaWduZWVOYW1lIjoiIiwiYXNzaWduZWVFbWFpbCI6IiIsImxpY2Vuc2VSZXN0cmljdG…

ComfyUI的优势+安装+基础介绍,干货内容,建议收藏!!!

简介 ComfyUI是一个基于节点流程的稳定扩散操作界面&#xff0c;通过流程实现了更加精准的工作流定制和完善的可复现性。 ComfyUI 与Webui 的区别&#xff1a; 一&#xff0c;界面设计&#xff1a; Web UI提供了直观友好的图形界面&#xff0c;适合初学者和非技术人员使用。…

JSON数据的解析与生成:深入Python的实践与应用

目录 一、JSON基础 1.1 JSON简介 1.2 Python中的JSON处理 二、解析JSON字符串到Python字典 2.1 使用json.loads()函数 2.2 异常处理 三、将Python字典转换为JSON字符串 3.1 使用json.dumps()函数 3.2 格式化输出 3.3 将Python对象写入文件 四、将Python对象写入JSON…

P01-何谓Java方法

P01-何谓Java方法 一、System.out.println()分析 二、剖析方法 谈到方法&#xff0c;我就突然想到了c函数&#xff1a; 其实&#xff1a;Java 方法和 C 函数在许多方面确实有类似之处&#xff0c;但它们也存在一些显著的差异。下面是它们的一些共同点和不同点&#xff1a; 共同…

POS机的接口

联迪的POS机 读取付款方式 private void Form_MisPos_Load(object sender, EventArgs e) { List<Trans> list MisPos.GetTransList(); int col 0; int y 15; int colcount 3; Panel p panel3;…

vben:对话框组件

本文将介绍如何在vben admin vue 框架中&#xff0c;创建对话框组件&#xff0c;并成功应用的代码。代码即插即用。 结果如下图所示&#xff0c; 一、创建对话框组件 创建 myModal/index.vue <template><div><BasicModalregister"registerModal"v-…