数据结构——红黑树(详解性质+C++模拟)

news2024/12/27 13:19:20

文章目录

  • 前言
  • 红黑树的概念
  • 红黑树的性质
  • 红黑树结点的定义
  • 红黑树的插入操作
    • 1. **按照二叉搜索树的规则插入新结点**
    • 2. 检测新节点插入后,红黑树的性质是否遭到破坏
  • 红黑树的验证
  • 总结

前言

本篇博客将为大家重点讲述红黑树这一数据结构,讲解其实现的方式即其具有的性质,并且最后用C++进行模拟实现这一数据结构,和AVL树相同,这篇文章也着重讲解关于其的插入操作。

由于其本质是一颗搜索二叉树,所以想要学习这一数据结构的同学需要首先了解二叉搜索树是什么,下面是博主以前写的关于二叉搜索树的博客链接,不清楚二叉搜索树是什么的伙伴可以先看看下面这篇博客:

博客链接: 数据结构 ——二叉搜索树(附C++模拟实现)

红黑树的概念

红黑树,是一种二叉搜索树,但在每个节点上增加了一个存储位表示节点的颜色,顾名思义可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
在这里插入图片描述

红黑树的性质

现在,我们的问题就是,为什么红黑树能保证:其最长路径种节点个数不会超过最短的两倍呢?
这就要从红黑树的性质出发了,看如下性质:

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

由于满足这五个性质,我们考虑最短路径的情况: 路径上的结点都是黑色的,最长路径:路径上的结点都是红黑相间的,那么又由于每条路径都有相同数量的黑色结点,所以一定有最长路径不超过最短路径的两倍。


在了解了性质之后,接下来就我们就边模拟实现红黑树边讲解其是如何操作来维护这些特性的把!

红黑树结点的定义

//用一个枚举常量来表示颜色
enum Color
{
	RED,
	BLACK
};

template<typename K, typename V>
struct RBTree_Node
{
	//为了提高对该结构操作的效率需要使用三叉链
	RBTree_Node<K, V>* _parent = nullptr;
	RBTree_Node<K, V>* _right = nullptr;
	RBTree_Node<K, V>* _left = nullptr;

	std::pair<K, V> _kv;
	int _color = RED;

	RBTree_Node(const std::pair<K,V>& kv)
		:_kv(kv)
	{}
};

这里有一个问题是,为什么要将结点的默认颜色给成红色

我们考虑插入操作,由于插入,我们有可能会破坏红黑树的性质,那么,大家觉得性质3和性质4哪个更好维护?
显然是性质3(不能有连续的红节点),而插入一个红节点一定不会破坏性质4,因此我们选择默认插入红节点。

红黑树的插入操作

与AVL树相同,红黑树也是在二叉搜索树的基础上加上其平衡条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索树的规则插入新结点

	bool insert(const std::pair<K, V>& kv)
	{
		//如果root是空节点,直接插入即可
		if (!_root)
		{
			node* newNode = new node(kv);
			//根节点的颜色一定是黑色
			newNode->_color = BLACK;
			_root = newNode;
			return true;
		}
		const K& newKey = kv.first;
		//寻找插入位置
		node* parent = nullptr, * cur = _root;
		while (cur)
		{
			const K& Key = cur->_kv.first;
			parent = cur;
			if (Key < newKey)
				cur = cur->_right;
			else if (Key > newKey)
				cur = cur->_left;
			else
				return false;
		}
		//到达此位置,说明插入的是一个新数据
		node* newNode = new node(kv);
		//插入一个红色节点,对旧树的影响最小
		//只需要修改连续的两个红色节点即可
		if (parent->_kv.first > newKey)
			parent->_left = newNode;
		else
			parent->_right = newNode;
		newNode->_parent = parent;
		cur = newNode;
		//新结点插入后,需要检查红黑树的性质是否遭到破坏,如果破坏,需要进行一系列的调整
		//....
		

2. 检测新节点插入后,红黑树的性质是否遭到破坏

因为新节点默认颜色是红色,因此:如果双亲结点的颜色是黑色,那就不需要继续调整,但当双亲结点颜色是红色的时候,就违反了不能有连续红节点的性质,此时需要分类讨论来调整红黑树:

在此之前,先对一些符号做一些解释:
cur: 当前结点p:父节点
g:祖父节点 u:叔叔结点(父结点的兄弟结点)

首先由于插入前一定是红黑树,所以如果p为红,g一定为黑,因此有些情况不存在

情况一:cur为红,p为红,g为黑,u存在且为红
下图种的a,b, c, d, e是任意红黑树子树,也可以是空
在这里插入图片描述
此时,为了同时维护性质3和性质4,我们把p和u变黑,然后让g变红,这样这两条路径的黑色结点数量相当于没有变化。

在这里插入图片描述
但是调整为g之后,g有可能是整棵树的根节点,也有可能是子树的根,而如果不是根节点,我们还需要继续向上判断是否有连续红节点存在,如果是根节点,直接将其变为黑色之后即可结束调整

//修改红色节点的逻辑
while (parent && parent->_color == RED)
{
	//双亲是红色,那么爷爷节点一定是黑色
	if (parent == _root)
	{
		parent->_color = BLACK;
		break;
	}
	node* grandfather = parent->_parent;
	//由于不知道p是g的左孩子还是右孩子,所以需要判断一下
	node* uncle = parent == grandfather->_left ? grandfather->_right : grandfather->_left;
	if (uncle && uncle->_color == RED)
	{
		//通过这一个操作就可以保证两条路径上的黑色节点的数量不变
		grandfather->_color = RED;
		uncle->_color = parent->_color = BLACK;
		//由于将爷爷节点变成红色,所以有可能会导致前面的节点也出现连续红节点的情形,需要继续判断
		cur = grandfather;
		parent = cur->_parent;
	}
	else
	{
		//...
	}

对于接下来两种情况,需要使用AVL树中的旋转操作,如果有不知道旋转操作如何实现的可以看看博主的另一篇博客:
链接 --> AVL详解
在这篇文章中有详细介绍旋转如何实现的部分,大家可以通过目录直接跳转观看即可。

情况二:cur为红,p为红,g为黑,u不存在/u存在且为黑

  1. u不存在
    在这里插入图片描述

如果u节点不存在,那么cur一定是新插入的结点,如果cur不是新插入的结点,那么根据性质3: cur和p一定有一个在调整前是黑色,那么以g为根结点的树就不满足红黑树的性质四:每条路径黑色结点个数相同

  1. u结点存在并且是黑色
    在这里插入图片描述

如果u结点存在并且是黑色,那么cur结点原来的颜色一定是黑色,现在是红色的原因是cur子树调整的过程中变成了红色。

这是由于如果cur是新增结点,那么在插入cur之前就已经不满足性质四,p,g路径上的黑色结点数目一定比u,g路径上的黑色结点数目少

对于这种情况,如果

p为g的左孩子,cur为p的左孩子,则进行右单旋操作
如果p为g的右孩子,cur为p的右孩子,则进行左单旋操作。
最后将p变成黑色,g变成红色即可完成调整
对于上图中的情况,旋转完之后得到:
在这里插入图片描述

我们来看一下为什么旋转之后能够维护红黑树的性质:
首先,很容易可以发现旋转只会对旋转前g的子树造成影响,并且通过图可以得知a, b, c, d, e这五个子树的路径上的黑色结点数量和旋转前并没有发生变化,因此这样子的操作是完全可行的。

并且如果我们不考虑各个结点上的值的话,这种做法可以理解为让p的左边路径少一个红节点,p的右边路径多一个红节点,并没有改变黑结点的数量,所以这个方法可行。

情况三:cur为红色,p为红,g为黑,u不存在/u存在且为黑
该情况和情况二不同的地方在于p和cur在其双亲的不同位置,对于这种情况,我们需要使用双旋的方法。
在这里插入图片描述

  1. 如果p为g的左孩子,cur为p的右孩子,则针对p做左单旋,之后就转换成了情况2,再使用一次右单旋即可恢复平衡
  2. 如果p为g的右孩子,cur为p的左孩子,则针对p做右单旋,同样转换为情况2,然后再使用一次左单旋即可。

上图第一步转换完成后:
在这里插入图片描述

由于这里的第一步是为了转换成情况2,所以我们只需要分析第一步是否会破坏红黑树的性质即可。

根据上图可知,第一步旋转完成之后,五颗子树的黑节点数量也斌没有发生变化,因此方法可行。

如此,我们就分析完了所有调整操作了,下面附上完整插入代码:


public:
	bool insert(const std::pair<K, V>& kv)
	{
		//如果root是空节点,直接插入即可
		if (!_root)
		{
			node* newNode = new node(kv);
			//根节点的颜色一定是黑色
			newNode->_color = BLACK;
			_root = newNode;
			return true;
		}
		const K& newKey = kv.first;
		//寻找插入位置
		node* parent = nullptr, * cur = _root;
		while (cur)
		{
			const K& Key = cur->_kv.first;
			parent = cur;
			if (Key < newKey)
				cur = cur->_right;
			else if (Key > newKey)
				cur = cur->_left;
			else
				return false;
		}
		//到达此位置,说明插入的是一个新数据
		node* newNode = new node(kv);
		//插入一个红色节点,对旧树的影响最小
		//只需要修改连续的两个红色节点即可
		if (parent->_kv.first > newKey)
			parent->_left = newNode;
		else
			parent->_right = newNode;
		newNode->_parent = parent;
		cur = newNode;
		//修改红色节点的逻辑
		while (parent && parent->_color == RED)
		{
			//双亲是红色,那么爷爷节点一定是黑色
			if (parent == _root)
			{
				parent->_color = BLACK;
				break;
			}
			node* grandfather = parent->_parent;
			//由于不知道p是g的左孩子还是右孩子,所以需要判断一下
			node* uncle = parent == grandfather->_left ? grandfather->_right : grandfather->_left;
			if (uncle && uncle->_color == RED)
			{
				//通过这一个操作就可以保证两条路径上的黑色节点的数量不变
				grandfather->_color = RED;
				uncle->_color = parent->_color = BLACK;
				//由于将爷爷节点变成红色,所以有可能会导致前面的节点也出现连续红节点的情形,需要继续判断
				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				//如果叔叔不存在或者叔叔节点位黑色,需要进行旋转操作
				bool parent_in_right = true, cur_in_right = true;
				if (grandfather->_left == parent) parent_in_right = false;
				if (parent->_left == cur) cur_in_right = false;
				if (parent_in_right && cur_in_right)
				{
					rotateL(grandfather);
					grandfather->_color = RED;
					parent->_color = BLACK;
				}
				else if (!parent_in_right && !cur_in_right)
				{
					rotateR(grandfather);
					grandfather->_color = RED;
					parent->_color = BLACK;
				}
				else if (parent_in_right && !cur_in_right)
				{
					rotateRL(grandfather);
					cur->_color = BLACK;
					grandfather->_color = RED;
				}
				else
				{
					rotateLR(grandfather);
					cur->_color = BLACK;
					grandfather->_color = RED;
				}
				_root->_color = BLACK;
				break;
			}
		}
		//如果插入节点的双亲是一个黑节点,不需要处理
		return true;
	}
private:
	void rotateL(node* parent)
	{
		node* cur = parent->_right, * curL = cur->_left;
		node* ppnode = parent->_parent;
		//连接cur和parent
		cur->_left = parent;
		parent->_parent = cur;
		//连接parent和curL
		parent->_right = curL;
		if (curL) curL->_parent = parent;
		//连接ppnode和cur
		cur->_parent = ppnode;
		if (!ppnode) _root = cur;
		else
		{
			if (ppnode->_left == parent)
				ppnode->_left = cur;
			else if (ppnode->_right == parent)
				ppnode->_right = cur;
			else
				throw "二叉树连接异常";
		}
	}
	void rotateR(node* parent)
	{
		node* cur = parent->_left, * ppnode = parent->_parent;
		node* curR = cur->_right;
		//连接cur和parent
		cur->_right = parent;
		parent->_parent = cur;
		//连接parent和curR
		parent->_left = curR;
		//注意判断curR为空的情况
		if (curR) curR->_parent = parent;
		//连接ppnode和cur
		cur->_parent = ppnode;
		if (!ppnode)
			_root = cur;
		else
		{
			if (ppnode->_right == parent)
				ppnode->_right = cur;
			else if (ppnode->_left == parent)
				ppnode->_left = cur;
			else
				throw "二叉树链接异常\n";
		}
	}
	void rotateRL(node* parent)
	{
		rotateR(parent->_right);
		rotateL(parent);
	}
	void rotateLR(node* parent)
	{
		rotateL(parent->_left);
		rotateR(parent);
	}

红黑树的验证

完成了红黑树的插入操作,我们当然要有办法验证手搓的红黑树是否满足其性质。
红黑树的检测分为两部分:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质
public:
	bool isRBTree() { return _isRBTree(_root); }
private:
	bool _isRBTree(node* root)
	{
		if (!root) return true;
		if (root->_color == RED) return false;
		bool RB1 = no_seriesRed(root);
		if (!RB1)
			return false;
		int blackNum = -1;
		bool RB2 = equalBlack(root, 0, blackNum);
		if (!RB2)
			return false;
		return true;
	}
	//验证没有连续的红节点
	bool no_seriesRed(node* root)
	{
		if (!root) return true;
		if (root->_color == RED)
			if (root->_parent && root->_parent->_color == RED)
			{
				std::cout << "连续红节点\n";
				return false;
			}
		return no_seriesRed(root->_left)
			&& no_seriesRed(root->_right);
	}
	//验证每条路径上的黑色结点数量是否都相同
	bool equalBlack(node* root, int now, int& blackNum)
	{
		if (!root)
		{
			if (blackNum == -1) blackNum = now;
			else if (now != blackNum)
			{
				std::cout << "黑节点数量不一致\n";
				return false;
			}
			return true;
		}

		if (root->_color == BLACK) now += 1;
		return equalBlack(root->_left, now, blackNum)
			&& equalBlack(root->_right, now, blackNum);
	}

总结

以上就是关于红黑树的所有内容啦,大家如果想要真正掌握红黑树,光看代码肯定是不够的,一定要自己下去模拟实践一次才能真正的掌握其核心,并且一定要深入理解它的性质!!以上就是本篇博客的所有内容啦,如果博主有哪里写的有问题或者有大家疑惑的地方,欢迎在评论区指出!!

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

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

相关文章

[NSSRound#1 Basic]sql_by_sql - 二次注入+布尔盲注||sqlmap

进入注册界面后   假设sql&#xff1a;update user set password ‘’ where username ‘’ and password ‘’     此时如果我们注册的用户名是admin’–、admin’#、admin’–的话   update user set password ‘123’ where username ‘admin’#’ and passwor…

[NISACTF 2022]hardsql - quine注入

题目描述&#xff1a;$password$_POST[passwd]; $sql"SELECT passwd FROM users WHERE usernamebilala and passwd$password;"; 从描述看出是quine注入&#xff0c;且用户名要是bilala 1、经测试&#xff0c;参数为&#xff1a;username&passwd&login登录&a…

重置系统后出现 press F12 to clear the tpm press Esc to reject this chan

案例分享&#xff1a; 外星人M15 R7重置系统后出现下图问题&#xff0c;暂时不能下一步。 原文如下&#xff1a; A configuration change was requested to Clear this computers TPM (Trusted platform module) warning:clearing erases information stored on the tpm.you …

正向代理和反向代理

正向代理和反向代理 1.正向代理和反向代理&#xff0c;squid&#xff0c;Nginx2.正向代理主要作用&#xff1a;3.反向代理主要作用&#xff1a;4.透明代理 1.正向代理和反向代理&#xff0c;squid&#xff0c;Nginx 1.用途不同&#xff1a;正向代理的典型用途是为在防火墙内的…

深度学习-卷积神经网络-AlexNET

文章目录 前言1.不同卷积神经网络模型的精度2.不同神经网络概述3.卷积神经网络-单通道4.卷积神经网络-多通道5.池化层6.全连接层7.网络架构8.Relu激活函数9.双GPU10.单GPU模型 1.LeNet-52.AlexNet1.架构2.局部响应归一化&#xff08;VGG中取消了&#xff09;3.重叠/不重叠池化4…

一文解释mapState的来龙去脉

mapState Vuex 提供的辅助函数之一&#xff0c;将 store 中的状态映射到组件的计算属性中&#xff0c;使得在组件中可以轻松地访问 Vuex store 中的状态值 MapState(映射状态) 在我们的 Count.vue 组件中&#xff0c;可以使用 mapState 来更简洁地获取 count 的状态值 首先&…

毕设-原创医疗预约挂号平台分享

医疗预约挂号平台 不是尚医通项目&#xff0c;先看项目质量&#xff08;有源码论文&#xff09; 项目链接&#xff1a;医疗预约挂号平台git地址 演示视频&#xff1a;医疗预约挂号平台 功能结构图 登录注册模块&#xff1a;该模块具体分为登录和注册两个功能&#xff0c;这些…

Android 开发错误集合

&#x1f525; 开发错误集合一 &#x1f525; Caused by: java.lang.ClassNotFoundException: Didnt find class "com.mask.app.ui.LoginRegisterActivity" on path: DexPathList[[zip file "/data/app/~~NMvHVhj8V6-HwGbh2amXDA/com.mask.app-PWbg4xIlETQ3eVY…

解密京东面试:如何应对Redis缓存穿透?

亲爱的小伙伴们&#xff0c;大家好&#xff01;欢迎来到小米的微信公众号&#xff0c;今天我们要探讨一个在面试中可能会遇到的热门话题——Redis缓存穿透以及如何解决它。这个话题对于那些渴望进入技术领域的小伙伴们来说&#xff0c;可是必备的哦&#xff01; 认识Redis缓存…

javaWeb蛋糕商城(前后台)

一、项目功能 1.前台功能 商品基本展示,包括推荐商品展示和类型商品展示.推荐商品包括条幅推荐,热销推荐和新品推荐.按照商品类型展示商品.商品详细信息展示.商品加入购物车.修改购物车内商品信息,例如数量等.用户登录.用户注册.修改个人信息,包括密码和收获信息.购物车付款.…

数值分析学习笔记——误差【华科B站教程版本】

误差 误差&#xff1a;一个物理量的真实值与计算值之间的误差 误差来源与分类 模型误差&#xff1a;对问题所抽象出来的数学/物理模型是误差的&#xff0c;比如要有一些假设条件才进行理论的推导观测误差&#xff1a;测量得到的模型的参数的值的误差方法误差&#xff08;截断…

React项目部署 - Nginx配置

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

保险业SAP转型:奠定坚实的基础

保险业面临着许多新的挑战&#xff1a;从新的市场参与者到人工智能和物联网。如何在这种快节奏的现实中满足客户的需求&#xff0c;降低成本&#xff0c;加速增长&#xff1f;首先&#xff0c;让我们考虑一下数字化转型成功是什么样子的&#xff0c;以及如何实现它。在这篇文章…

Python 列表推导式深入解析

Python 列表推导式深入解析 列表推导式是 Python 中的一种简洁、易读的方式&#xff0c;用于创建列表。它基于一个现有的迭代器&#xff08;如列表、元组、集合等&#xff09;来生成新的列表。 基本语法&#xff1a; 列表推导式的基本形式如下&#xff1a; [expression for…

HJ33整数与IP地址间的转换

描述 原理&#xff1a;ip地址的每段可以看成是一个0-255的整数&#xff0c;把每段拆分成一个二进制形式组合起来&#xff0c;然后把这个二进制数转变成 一个长整数。 举例&#xff1a;一个ip地址为10.0.3.193 每段数字 相对应的二进制数 10 000…

程序三高的方法

程序三高的方法 目录概述需求&#xff1a; 设计思路实现思路分析1.1&#xff09;高并发 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,c…

【SpringBoot】多环境配置和启动

环境分类&#xff0c;可以分为 本地环境、测试环境、生产环境等&#xff0c;通过对不同环境配置内容&#xff0c;来实现对不同环境做不同的事情。 SpringBoot 项目&#xff0c;通过 application-xxx.yml 添加不同的后缀来区分配置文件&#xff0c;启动时候通过后缀启动即可。 …

[架构之路-231]:计算机硬件与体系结构 - 性能评估汇总,性能优化加速比

目录 一、计算机体系结构 二、计算机性能评估 2.1 分类方法1 2.2 分类方法2 三、常见的专项性能测试工具 3.1 浮点运算性能&#xff08;FLOPS&#xff09; 3.2 综合理论性能法 3.3 历史基准测试&#xff08;跑分软件&#xff09;&#xff1a;通过运行典型的综合性的程序…

012-第二代硬件选型

第二代硬件选型 文章目录 第二代硬件选型项目介绍重新换平台缘由X86 && Arm 架构切换 ARM Linux 硬件选型系统确定Qt 版本确定总结一下 关键字&#xff1a; Qt、 Qml、 Arm、 X86、 linux 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QM…

Java笔记七(封装,继承与多态)

封装 该露的露&#xff0c;该藏的藏 程序设计追求“高内聚&#xff0c;低耦合”。高内聚就是类的内部数据操作细节自己完成&#xff0c;不允许外部干涉&#xff1b;低耦合&#xff1a;仅暴露少量的方法给外部使用 封装&#xff08;数据的隐藏&#xff09; 通常&#xff0c;…