【C++深度探索】红黑树的底层实现机制

news2024/11/15 12:04:10
🔥 个人主页:大耳朵土土垚
🔥 所属专栏:C++从入门至进阶

这里将会不定期更新有关C/C++的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉

前言

  红黑树与AVL树一样,也是一种自平衡的二叉搜索树,它在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black,通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

文章目录

  • 前言
  • 1.红黑树结构
  • 2.红黑树的插入
  • 3.红黑树的验证
  • 4.中序遍历
  • 5.结语

1.红黑树结构


红黑树的性质

  • 每个结点不是红色就是黑色
  • 根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点

所以红黑树的节点必须包含一个值类存储该节点的颜色,我们可以利用枚举来实现:

//枚举颜色
enum Colour
{
	RED,
	BLACK
};

//节点类
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;	//存放数据
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;	//保存颜色

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

//红黑树类
template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
	bool Insert(const pair<K, V>& data);
		
	// 检测红黑树是否为有效的红黑树
	bool IsValidRBTRee();

	//中序遍历
	void InOrder()
	{
		_InOrder(_pHead);
	}

private:
	void _InOrder(Node* root);
	
	bool Check(Node* root, int blackNum, const int refNum);
	
	// 左单旋
	void RotateL(Node* parent);
	// 右单旋
	void RotateR(Node* parent);
	
private:
	Node* _pHead = nullptr;
};


2.红黑树的插入


红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
  1. 按照二叉搜索的树规则插入新节点
  2. 检测新节点插入后,红黑树的性质是否造到破坏,如果破坏进行相应的修改操作

在插入新节点时,我们先确定一下新节点的颜色,如果是黑色,那么在插入后该条子路径上就会多一个黑色节点,根据红黑树的性质需要在其他路径上都增加一个新节点才可以,比较麻烦,所以我们将新节点的颜色设为红色,这样如果其父亲是黑色就刚刚好插入成功,如果父亲是红色我们就再来修改;所以我们将新节点的颜色设置为红色:

//节点类
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;	//存放数据
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;	//保存颜色

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_col(RED)		//直接在构造时设置即可
	{}
};

先正常插入节点:

//1.先找到插入位置
//如果是空树
if (_pHead == nullptr)
{
	Node* newnode = new Node(data);
	newnode->_col = BLACK;
	_pHead = newnode;
	return true;
}
//如果不是空树
Node* cur = _pHead;
Node* parent = nullptr;
while (cur)
{
	if (cur->_kv.first > data.first)
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (cur->_kv.first < data.first)
	{
		parent = cur;
		cur = cur->_right;
	}
	else
		return false;//没找到返回false
}

//2.找到,插入节点
Node* newnode = new Node(data);
//判断插入父节点左侧还是右侧
if (parent->_kv.first > data.first)
	parent->_left = newnode;
else
	parent->_right = newnode;

//更新newnode父节点
newnode->_parent = parent;
  1. 如果父节点是黑色,那么直接插入节点即可:
if (parent->_col == BLACK)
{
	//父节点是黑色,插入成功
	return true;
}
  1. 如果父节点是红色,那么我们需要调整:

因为不可能有两个红色连在一起,所以我们需要进行调整;而且父节点是红色的话那么父节点肯定不是根节点且其父节点的颜色也只能是黑色,如下图所示:

这时,我们就需要根据叔叔节点来进行调整节点:

  • 如果uncle节点是红色:

我们就可以将unlcle和parent节点都变为黑色,grandparent节点变为红色:

这样这两条路径的黑色节点依然是一个,没有变,但是grandparent节点变为红色,如果它的父节点是黑色那么调整成功,但是如果其父节点是红色,红黑树的性质就不满足,所以我们需要继续向上调整。

  • 如果uncle节点是黑色:

这时我们发现uncle节点的路径上多了一个黑色节点,说明cur节点不可能是新增节点,这种情况是由上面uncle节点是红色情况调整之后还需要继续向上调整得来的(cur是上面情况的grandparent,grandparent的父节点也是红色),单纯的变色已经不能维持红黑树的性质,我们需要进行旋转:


情况一:如果parent为grandparent的左孩子,cur为parent的左孩子,则进行右单旋转:

  再将grandparent的颜色改为红色,parent改为黑色。

情况二:如果parent为grandparent的右孩子,cur为parent的右孩子,则进行左单旋转:

  再将grandparent的颜色改为红色,parent改为黑色。

情况三:如果parent为grandparent的左孩子,cur为parent的右孩子,则先进行左单旋转换成情况一,再进行右单旋:

  再像情况一进行右单旋:

  再将grandparent的颜色改为红色,cur改为黑色。

情况四:如果parent为grandparent的右孩子,cur为parent的左孩子,则先进行右单旋转换成情况二,再进行左单旋:

  再像情况二进行左单旋:

  再将grandparent的颜色改为红色,cur改为黑色。


✨进行旋转后,红黑树就满足了性质,插入成功

  • 如果uncle不存在:

这种情况和uncle存在且为黑是一样的,所以可以并入上面一起考虑。


完整代码如下:

bool Insert(const pair<K, V>& data)
{
	//1.先找到插入位置
	//如果是空树
	if (_pHead == nullptr)
	{
		Node* newnode = new Node(data);
		newnode->_col = BLACK;
		_pHead = newnode;
		return true;
	}
	//如果不是空树
	Node* cur = _pHead;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first > data.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < data.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
			return false;//没找到返回false
	}
	
	//2.找到,插入节点
	Node* newnode = new Node(data);
	//判断插入父节点左侧还是右侧
	if (parent->_kv.first > data.first)
		parent->_left = newnode;
	else
		parent->_right = newnode;

	//更新newnode父节点和颜色
	newnode->_parent = parent;
	if (parent->_col == BLACK)
	{
		//父节点是黑色,插入成功
		return true;
	}
	if (parent->_col == RED)
	{
		//父节点是红色
		cur = newnode;
		while (parent && parent->_col == RED)
		{
			Node* grandparent = parent->_parent;//parent是红色,肯定不是根节点,所以grandparent不是空节点,而且是黑色
			
			//找叔叔节点
			Node* uncle = grandparent->_left;
			if (parent == grandparent->_left)
				uncle = grandparent->_right;
			
			if (uncle&&uncle->_col == RED)
			{
				//如果uncle是红色
				//将unlcle和parent节点都变为黑色,grandparent节点变为红色
				parent->_col = uncle->_col = BLACK;//即可保证所有路径上黑色一样多
				grandparent->_col = RED;
			
				//继续往上更新
				cur = grandparent;
				parent = cur->_parent;
			}
			else if (uncle==nullptr||uncle->_col == BLACK)
			{
				//如果uncle不存在或者存在且为黑色
				if (grandparent->_left == parent && parent->_left == cur)
				{
					//右单旋,再将grandparent改为红色,parent改为黑色
					RotateR(grandparent);
					grandparent->_col = RED;
					parent->_col = BLACK;
				}
				else if (grandparent->_right == parent && parent->_right == cur)
				{
					//左单旋,再将grandparent改为红色,parent改为黑色
					RotateL(grandparent);
					grandparent->_col = RED;
					parent->_col = BLACK;
				}
				else if (grandparent->_right == parent && parent->_left == cur)
				{
					RotateR(parent);//先右单旋
					RotateL(grandparent);//再左单旋
					//再将grandparent的颜色改为红色,cur改为黑色
					grandparent->_col = RED;
					cur->_col = BLACK;
				}
				else if (grandparent->_left == parent && parent->_right == cur)
				{
					RotateL(parent);//先左单旋
					RotateR(grandparent);//后右单旋
					//再将grandparent的颜色改为红色,parent改为黑色
					grandparent->_col = RED;
					cur->_col = BLACK;
				}
				else
					assert(false);
				
				//插入成功,跳出循环
				break;
			}
		}

	}
	_pHead->_col = BLACK;//最后不管怎样,根节点都是黑色
	return true;
}

因为涉及到多种情况,所以根节点的颜色可能会顾及不上,所以最后我们可以加一句_pHead->_col = BLACK;,这样不管怎么样,根节点都是黑色了。


左、右单旋函数与AVL树的左、右单旋一样:

// 左单旋
void RotateL(Node* parent)
{

	Node* cur = parent->_right;

	//将cur的左边给parent的右边,cur的左边再指向parent
	parent->_right = cur->_left;
	cur->_left = parent;

	//链接cur与parent的父节点
	if (parent->_parent == nullptr)
	{
		//如果parent是根节点
		cur->_parent = nullptr;
		_pHead = cur;
	}
	else if (parent->_parent->_left == parent)
		parent->_parent->_left = cur;
	else
		parent->_parent->_right = cur;


	//更新父节点
	cur->_parent = parent->_parent;
	parent->_parent = cur;
	if (parent->_right)//判断parent的右边是否存在
		parent->_right->_parent = parent;

	
}
// 右单旋
void RotateR(Node* parent)
{
	Node* cur = parent->_left;

	//将cur的右边给parent的左边,cur的右边再指向parent
	parent->_left = cur->_right;
	cur->_right = parent;

	//链接cur与parent的父节点
	if (parent->_parent == nullptr)
	{
		//如果parent是根节点
		cur->_parent = nullptr;
		_pHead = cur;
	}
	else if (parent->_parent->_left == parent)
		parent->_parent->_left = cur;
	else
		parent->_parent->_right = cur;


	//更新父节点
	cur->_parent = parent->_parent;
	parent->_parent = cur;
	if (parent->_left)
		parent->_left->_parent = parent;
}

红黑树的左、右单旋与AVL树的区别在于不需要跟新平衡因子。


测试函数:

void RBTreeTest()
{
	RBTree<int, int> t;
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
}

3.红黑树的验证


 红黑树的验证和AVL树一样,分为两个步骤:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质

对于第二点:

// 检测红黑树是否为有效的红黑树
bool IsValidRBTRee()
{
	if (_pHead == nullptr)
		return true;

	if (_pHead->_col == RED)
	{
		return false;
	}

	// 先求一条路径上黑色节点数量作为参考值
	int refNum = 0;
	Node* cur = _pHead;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++refNum;
		}

		cur = cur->_left;
	}

	return Check(_pHead, 0, refNum);
}

首先如果一棵树是空树满足红黑树的性质,返回true;其次如果根节点为红色则不满足红黑树的性质,返回false;然后再根据每条路径上是否有相同的黑色节点已及是否存在连续的红色节点来进一步判断即Check()函数,但是我们需要先确定一条路上应该有多少个黑色节点作为参考。


Check()函数如下:

bool Check(Node* root, int blackNum, const int refNum)
{
	if (root == nullptr)
	{
		//cout << blackNum << endl;
		if (refNum != blackNum)
		{
			cout << "存在黑色节点的数量不相等的路径" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << root->_kv.first << "存在连续的红色节点" << endl;
		return false;
	}

	if (root->_col == BLACK)
	{
		blackNum++;
	}

	return Check(root->_left, blackNum, refNum)
		&& Check(root->_right, blackNum, refNum);
}

因为Check()函数使用的是递归来计算每条路径上黑色节点的数量,所以当root为空时我们就可以将计算该条路径上的黑色节点数量blackNum与参考值refNum进行比较,如果相等返回true,不相等就返回fals;此外如果在计算黑色节点过程中存在连续的红色节点也直接返回false即可。


测试函数:

void RBTreeTest()
{
	RBTree<int, int> t;
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		t.Insert({ e, e });
	}
	
	cout << t.IsValidRBTRee() << endl;
}

4.中序遍历


 与二叉搜索树一样,可以使用递归进行中序遍历,并且遍历结果是有序的,代码如下:

//中序遍历
void InOrder()
{
	_InOrder(_pHead);
}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

结果如下:


5.结语


  因为红黑树也是二叉搜索树,其他的类似查找节点,析构函数和构造函数都与二叉搜索树类似,对于删除节点,可按照二叉搜索树的方式将节点删除,然后再进行调整,大家有兴趣可以自己查找了解一下,以上就是今天所有的内容啦~ 完结撒花 ~🥳🎉🎉

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

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

相关文章

VNC未授权访问漏洞

VNC 是虚拟网络控制台Virtual Network Console的英文缩写。它是一款优秀的远程控制工具软件由美国电话电报公司AT&T的欧洲研究实验室开发。VNC是基于 UNXI 和 Linux 的免费开源软件由 VNC Server 和 VNC Viewer 两部分组成。VNC 默认端口号为 5900、5901。VNC 未授权访问漏…

STM32F407移植LVGL(V8.3版本)

一、LVGL简述 1.丰富且强大的模块化图形组件&#xff1a;按钮、图表、列表、滑条、图片等 2.高级图形引擎&#xff1a;动画、抗锯齿、透明度、平滑滚动、图层混合等效果 3.支持多种输入设备&#xff1a;触摸屏、键盘、编码器、按键等 4.配置可裁剪&#xff0c;最低资源占用&am…

latex中Function函数报错

latex写伪码时&#xff0c;发现报错&#xff0c;截图如下&#xff1a; 解决办法&#xff0c;添加宏包&#xff0c;截图如下&#xff1a; \usepackage{algpseudocode}

虚幻引擎 C++ 实现平面阴影

1、平面阴影介绍 平面阴影是一种相对简单的渲染阴影的方式&#xff0c;可以理解为对一个模型渲染两次&#xff0c;一次是渲染模型本身&#xff0c;另一次是渲染模型的投影。渲染投影可以看作是将模型的顶点变换到地面的投影空间再渲染&#xff0c;可以理解为渲染了一个“压扁”…

pytorch学习笔记6 tensor拼接和拆分

cat 合并 dim必须首选相同&#xff08;上例都是3&#xff09;&#xff0c;其次除了合并的dim&#xff08;上例中为dim0&#xff09;外&#xff0c;其它dim的size必须相同&#xff08;dim 1的size是32&#xff0c;dim2的size是8&#xff09;&#xff0c;否则需要手动处理到相同…

vue3 + Spingboot + oracle 通过Base64存储图片

一 、前言 近期在做vue3 Springboot oracle 的工作&#xff0c;有个小功能通过页面导入图片保存到oracle数据库中&#xff0c;本人对前端不是很熟悉&#xff0c;借此记录一下实现方法&#xff1b; 二、前端部分代码 <template><div class"dialog-mian"&…

SQL注入实例(sqli-labs/less-7)

0、初始页面 1、确定闭合字符 确定闭合字符为单引号括号括号 )) ?id1 and 11 ?id1 and 12 ?id1 ?id1)) 2、查看securie_file_priv参数 ?id1)) and upddatexml(1,concat(0x7e,(select secure_file_priv),0x7e),1) -- 3、写入一句话木马 ?id1)) union select null,&q…

SFT、RLHF、DPO、IFT —— LLM 微调的进化之路

TL;DR • SFT、RLHF 和 DPO 都是先估计 LLMs 本身的偏好&#xff0c;再与人类的偏好进行对齐&#xff1b; • SFT 只通过 LLMs 生成的下一个单词进行估计&#xff0c;而 RLHF 和 DPO 通过 LLMs 生成的完整句子进行估计&#xff0c;显然后者的估计会更准确&#xff1b; • 虽然…

壹连科技净利润增速放缓:毛利率清一色下滑,研发费用率远弱同行

《港湾商业观察》施子夫 王璐 从2022年6月20日递表创业板以来&#xff0c;深圳壹连科技股份有限公司&#xff08;以下简称&#xff0c;壹连科技&#xff09;已经走了2年多的历程&#xff0c;如今离挂牌上市近在咫尺。 今年7月22日&#xff0c;壹连科技提交了注册申请。8月2日…

哪个电脑桌面便签好用并且无广告弹窗?

在日常生活和工作中&#xff0c;很多人喜欢在电脑桌面上使用便签软件。便签软件可以方便地记录临时任务、重要信息或者待办事项&#xff0c;帮助用户更好地管理时间和提高工作效率。想象一下&#xff0c;在繁忙的工作中&#xff0c;你能够快速在桌面便签上记下即将要做的任务&a…

基本K8s搭建Jekins+gitee项目自动部署

这里写目录标题 1.基本K8s部署安装Jekins2.设置Jenkins国内镜像源2.安装Gitee插件1.安装Gitee Plugin2.验证安装Gitee Plugin 3.新建任务1.输入任务名称2.输入你gitee上的项目链接3.测试构建 4.查看项目在k8s集群master节点的位置1.确认 Jenkins Pod 名称2.使用kubectl exec到 …

大数据技术复习--概述

概述 数据的概念&#xff1a;数据是指对客观事件进行记录并可以鉴别的符号&#xff0c;是对客观事物的性质、状态以及相互关系等进行记载的物理符号或这些物理符号的组合&#xff0c;是可识别的、抽象的符号。 数据类型&#xff1a;文本、图片、音频、视频 从数据的结构化程…

2024华数杯全国大学生数学建模竞赛B题思路-VLSI电路单元的自动布局-MIA 感知的详细布局问题描述

本章主要对超大规模集成电路&#xff08;Very Large Scale Integration Circuit&#xff0c;VLSI&#xff09;布局 问题进行了描述&#xff0c;首先简单梳理一下超大规模集成电路设计流程、物理设计相关的知 识&#xff0c;接着对 MIA 感知的混合高度单元集成电路详细布局问题的…

vue之ref 属性

文章目录 1.ref 属性概述1.1 作用和特点 2.vue2用法2.1 获取 dom2.3 获取组件&#xff1a; 3.vue3用法 1.ref 属性概述 1.1 作用和特点 &#xff08;1&#xff09;作用&#xff1a;用来给元素或组件注册引用信息(相当于是id的替代者) &#xff08;2&#xff09;应用在HTML标签…

Kettle下载安装MySQL驱动教程

在 Windows 系统上下载适用于 MySQL 的 JDBC 驱动程序&#xff0c;您可以按照以下步骤操作&#xff1a; 1. 访问 MySQL 官方下载页面 打开浏览器&#xff1a; 打开您喜欢的浏览器。 访问 MySQL Connector/J 下载页面&#xff1a; 访问 MySQL Connector/J 下载页面. 2. 选择…

探索亚马逊Amazon S3:无缝存储管理与极速数据传输的奥秘

亚马逊云科技中Amazon S3&#xff0c;因其设计简单与高度可靠&#xff0c;允许用户通过互联网存储和检索任意数量的数据&#xff0c;并能够自动扩展以满足各种规模的需求&#xff0c;使得Amazon S3成为了许多云计算应用和网站的核心存储基础设施之一&#xff0c;Amazon S3提供的…

WSL2安装多个Ubuntu实例,大佬带你玩转Linux!!!

安装wsl子系统并安装一个Ubuntuwsl ubuntu 安装的正确方式-CSDN博客文章浏览阅读546次,点赞10次,收藏4次。wsl ubuntu 安装的正确方式:将wsl2设置为默认版本:1、打开powershell2、设置wsl的版本为2​编辑3、更新wsl程序4、强制关闭子系统5、查看wsl支持的列表6、安装指定版…

【Dynamo】AnyCAD使用Dynamo绘制三维模型(二)——生成序列和范围的几种方式

说明&#xff1a; Dynamo为开源项目&#xff0c;开源地址&#xff1a;https://github.com/DynamoDS/Dynamo.git本文章使用版本&#xff1a;v3.0.3 范围 使用Range节点 start和end分别表示范围的边界&#xff0c;step表示步长。如下为[1,10]范围内步长为2结果 ​ 使用Code…

[000-01-025].第07节:WorkBench

我的后端学习大纲 我的Drools学习大纲 8. WorkBench 8.1 WorkBench简介: 1.WorkBench是KIE组件中的元素&#xff0c;也称为KIE-WB&#xff0c;是Drools-WB与JBPM-WB的结合体。它是一个可视化的规则编辑器。WorkBench其实就是一个war包&#xff0c;安装到tomcat中就可以运行。…

UE Sequence学习

UE4中的动画编辑器 —— Sequencer in UE4 - 知乎 (zhihu.com) UE4 LevelSequence源码解析 - 知乎 (zhihu.com) C模块 对ue4 sequence的学习和理解 - 知乎 (zhihu.com) 必须要先在你项目工程的.build.cs里加入 MoviePlayer, LevelSequence, MovieScene. 引入头文件 #inclu…