数据结构进阶 红黑树

news2025/1/19 7:52:48

作者:@小萌新
专栏:@数据结构进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍高阶数据结构: 红黑树
在这里插入图片描述

红黑树

  • 红黑树的概念
  • 红黑树的性质
  • 红黑树节点的定义
  • 红黑树的插入
    • 情况一
    • 情况二
    • 情况三
  • 红黑树的验证
  • 红黑树的查找
  • 红黑树的删除
  • 红黑树与AVL树的比较

红黑树的概念

红黑树是一种二叉搜索树 但在每个结点上增加一个存储位表示结点的颜色 可以是Red或Black

通过对任何一条从根到叶子的路径上各个结点着色方式的限制 红黑树确保没有一条路径会比其他路径长

出两倍 因而是接近平衡的

在这里插入图片描述

红黑树的性质

红黑树有以下五点性质

  1. 根节点是黑色的
  2. 每个节点不是黑色就是红色
  3. 如果一个节点是红色的 那么它的两个子节点就是黑色的
  4. 对于每个节点 从该节点到其所有后代节点的简单路径上 均包含相同数目的黑色节点
  5. 每个叶子节点都是黑色的 (这里的叶子节点指的是空节点)

了解了上面这五条性质之后我们这里抛出一个问题

红黑树是如何确保 没有一条路径会比其他路径长出两倍 这个性质的呢?

下面是关于这个问题的解答

根据红黑树的性质三 我们可以推断出不会有连续存在的红节点
根据红黑树的性质四 我们可以推断出每一条路径上的黑色节点数目相同
结合我们的推断一 和 推断二 我们可以得出以下的结论
红黑树的最短路径一定是全黑的
红黑树的最长路径一定是红黑相间的
再根据上面的结论一和结论二 我们假设最短路径的长度是N 那么最长路径的长度就是2N
所以我们现在就能得出结论 没有一条路径会比其他路径长出两倍

红黑树节点的定义

我们这里实现KV模型三叉链结构的红黑树 此外增加一个成员变量颜色来控制红黑

关于颜色的成员变量 我们可以使用之前学过的枚举来解决

enum Colour
{
	RED,
	BLACK
};

那么接下来我们只需要定义一个枚举变量来控制颜色就好了

红黑树节点类的定义如下

template<class K,class V>
class RBTreeNode
{
public:
	// 三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	// 默认插入颜色为红 至于为什么 后面会解释
	Colour _col;

	// 储存键值对
	pair<K, V> _kv;

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

	}
private:

};

那么再这里我们再详细解释一下这句代码 也就是为什么_col默认设置为红

  _col(RED)

首先 我们回顾下红黑树的性质四

对于每个节点 从该节点到其所有后代节点的简单路径上 均包含相同数目的黑色节点

也就是说 假如我们插入的是黑色的节点

那么势必会造成插入的节点路径上的黑色节点变多了

那么根据上面的性质四 我们就要更新所有路径上的黑色节点数目以保持红黑树的性质

其次 我们来回顾下红黑树的性质三

如果一个节点是红色的 那么它的两个子节点就是黑色的

也就是说 假设我们插入的是红色的节点

如果它的父节点是黑色 那么插入之后仍然满足红黑树的性质

如果它的父节点是红色 那么此时我们需要调整整颗树使其满足红黑树的性质

综上所述 我们在选择默认插入节点为红色的时候 需要调整的次数较少 插入较为方便 故而插入节点默认选红

红黑树的插入

红黑树的插入逻辑总共分为三步

  1. 按照二叉搜索树的插入规则找到待插入节点
  2. 将待插入节点插入到树中
  3. 如果插入节点的父节点为红色 则执行调整操作 若为黑色 则无需操作

前面两点和二叉搜索树的插入操作相同 所以说我们插入操作的关键在如何实现第三点

我们来看具体的情况

假设插入节点的父节点为黑色

那么我们找到位置之后直接插入红色节点即可 因为插入红色节点之后不会破坏红黑树的性质

假设插入节点的父节点为红色

那么这个时候就有点复杂了 因为根据红黑树的性质三 我们可以推断出 是不可能存在两个连续的红色节点的

所以说这个时候我们必须要对红黑树进行调整 来让这棵树继续满足红黑树的性质

至于红黑树应该怎么调整 这里分为三种情况来讨论

情况一

插入节点的叔叔节点存在 且插入节点的叔叔节点的颜色为红色

注: 我们将插入节点称为cur
将其父节点称为 p
将与其父节点平辈的节点称为 u
将其父亲的父亲节点称为 g

示例图如下

在这里插入图片描述
此时 为了避免连续的红色节点 我们将cur的父亲和叔叔节点都变成黑色

这时候还没有完 由于我们将父亲节点和叔叔几点都变成了黑色 此时这条路径上的黑色节点多了一个

要变成原来的黑色节点个数 我们必须要减少一个黑色节点的数量

此时 我们只需要将它的爷爷节点变成红色就可以

在这里插入图片描述

但是这种情况下又会出现一种问题

在爷爷节点并不是根节点的情况下 有可能爷爷节点的父节点还是红色

这样就破坏了 红黑树的性质三

所以说此时我们需要将爷爷节点当作是新插入的节点 再根据不同的情况继续向上调整

情况二

插入节点的叔叔节点存在 并且插入节点的叔叔节点的颜色为黑色

我们首先来看示例图

在这里插入图片描述
假设cur是新增节点

如图 我们选择了两条路线对于这两条路线来说 根节点走到爷爷节点之前 由于走的是同一条路线

所以说黑色节点的数目都是相同的 而当我们从爷爷节点开始找黑色节点

则左边一定是只有两个黑色节点 (爷爷节点和最后的空节点)

而右边由于叔叔节点的存在一定是大于等于三个黑色节点的

所以说此时不满足性质四

所以我们可以得出结论cur一定不是新增节点

那么既然cur不是新增节点 那么就只有一种可能了 cur是由我们的情况一变换而来

此时的这种情况 我们通过变色操作已经处理不了了 只能通过旋转

如果说祖孙三代在同一条直线上 则我们只需要执行单旋操作就可以

操作图如下

在这里插入图片描述
当然 如果祖孙三在右边就执行左单旋操作

如果祖孙三代是一条折线就执行双旋操作

就像下面这样子
在这里插入图片描述
如果出现这种情况就执行左右双旋

图片表示如下
在这里插入图片描述

这里我们是不是发现和上面右单旋的情况类似了 之后只要执行下右单旋 然后变色就行了

当然 如果祖孙三代的折线方向与上面相反就执行右左双旋操作

情况三

插入节点的叔叔不存在

在这里插入图片描述
此时一定是新增节点而出现的情况

因为如果是由情况一变化而来 则下面父亲节点下面必定有黑色节点 此时不满足红黑树的性质四

在这种情况我们还是一样 分为直线和折线情况讨论

像上面这张图一样的偏左直线则我们执行右单旋然后变色

在这里插入图片描述

当然 如果祖孙三在右边就执行左单旋操作

如果出现这种折线情况则指向左右双旋操作

在这里插入图片描述
这里我们是不是发现和上面右单旋的情况类似了 之后只要执行下右单旋 然后变色就行了

当然 如果祖孙三代的折线方向与上面相反就执行右左双旋操作

完整的红黑树插入代码如下

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* parentParent = parent->_parent;

		//建立subLR与parent之间的联系
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		//建立parent与subL之间的联系
		subL->_right = parent;
		parent->_parent = subL;

		//建立subL与parentParent之间的联系
		if (parentParent == nullptr)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == parentParent->_left)
			{
				parentParent->_left = subL;
			}
			else
			{
				parentParent->_right = subL;
			}
			subL->_parent = parentParent;
		}
	}

	//左右双旋
	void RotateLR(Node* parent)
	{
		RotateL(parent->_left);
		RotateR(parent);
	}

	//右左双旋
	void RotateRL(Node* parent)
	{
		RotateR(parent->_right);
		RotateL(parent);
	}

	pair<Node*, bool> Insert(const pair<K, V>& kv)
	{
		// 先考虑空树的情况 
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK; // 因为我们默认插入节点的颜色是红色 而根节点是黑色 所以说要特殊处理
			return make_pair(_root, true);
		}

		// 如果不是空树则按照二叉搜索树的方法找到待插入的位置 

			//1、按二叉搜索树的插入方法,找到待插入位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kv.first < cur->_kv.first) //待插入节点的key值小于当前结点的key值
			{
				//往该节点的左子树走
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first) //待插入节点的key值大于当前结点的key值
			{
				//往该节点的右子树走
				parent = cur;
				cur = cur->_right;
			}
			else //待插入节点的key值等于当前节点的key值
			{
				return make_pair(cur, false); //插入失败
				// 唯一需要注意的一点 我们要返回插入失败位置的迭代器
			}
		}

		// 将待插入的节点插入到树中
		cur = new Node(kv);
		Node* newnode = cur;// 记录新插入的节点 
		if (kv.first < parent->_kv.first) //新节点的key值小于parent的key值
		{
			//插入到parent的左边
			parent->_left = cur;
			cur->_parent = parent;
		}
		else //新结点的key值大于parent的key值
		{
			//插入到parent的右边
			parent->_right = cur;
			cur->_parent = parent;
		}

		// 如果父节点是红色 则开始对红黑树进行处理
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->parent; // 因为parent是红色 所以说它一定存在父节点
			if (parent == grandfather->_left) // parent是grandfather的左
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED) // 对应情况一
				{
					// 颜色调整
					parent->_col == BLACK;
					uncle->_col == BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = parent;
					parent = cur->_parent;
				}
				else // 对应情况二和情况三
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather); // 右单旋

						// 颜色调整
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateLR(grandfather); // 左右双旋

						// 颜色调整
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
				}
				break;
			}
			else //parent是grandfather的右孩子
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED) // 对应情况一
				{
					parent->_col = BLACK;
					uncle->_col = RED;

					cur = parent;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						RotateRL(grandfather);

						// 颜色调整
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(grandfather);

						// 颜色调整
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					break;
				}
			}

			_root->_col = BLACK; // 根节点有可能被情况一改成红色 以防万一
			return make_pair(newnode, true); //插入成功
		}
	}

红黑树的验证

红黑树首先一棵二叉搜索树 所以说我如果我们中序遍历之 则其是有序的

代码如下


	void _Inorder(Node* root)
	{
		// 先考虑中止条件
		Node* cur = _root;
		if (cur == nullptr)
		{
			return;
		}


		_Inorder(cur -> _left);
		cout << cur->_kv.first << " : " << cur->_kv.second << endl;
		_Inorder(cur->_right);
	}

此外它还需要满足红黑树的所有性质 验证代码如下

	bool ISRBTree()
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
		{
			cout << "error:根结点为红色" << endl;
			return false;
		}

		// 找出最左路径的黑色节点作为参考值
		Node* cur = _root;
		int balckcount = 0;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				count++;
			}
			cur = cur->_left;
		}


		int count = 0;
		return _ISRBTree(_root, count, balckcount);
	}

	bool _ISRBTree(Node* root, int count, int blackcount)
	{
		if (root == nullptr)
		{
			return count == blackcount;
		}
		if (root->_col == RED && root->_parent == RED)
		{
			return false;
		}

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

		return _ISRBTree(root->_left, count, blackcount) && _ISRBTree(root->right, count, blackcount);
	}

红黑树的查找

红黑树的查找和二叉搜索树的查找类似

  1. 如果查找的树是空树 则查找失败
  2. 如果key值小于当前节点的值 则向左查找
  3. 如果key值大于当前节点的值 则向右查找
  4. 如果key值等于当前节点的值 则返回当前节点指针

代码如下

//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_kv.first) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了目标结点
		{
			return cur; //返回该结点
		}
	}
	return nullptr; //查找失败
}

红黑树的删除

红黑树的删除满足下面几个步骤

  1. 遵循二叉搜索树的删除原则 找到待删除位置
  2. 找到删除位置之后 删除节点
  3. 看看删除后的二叉树是否是红黑树 如果不是调整之

红黑树的删除并不是我们学习的重点内容 这里也是和AVL树一样跳过

红黑树与AVL树的比较

红黑树和AVL树都是很高效的AVL树 它们增删查改的时间复杂度都是Log(N)

但是它们实现控制二叉树平衡的方式不同

  • AVL树通过平衡因子来实现二叉搜索树的严格平衡
  • 红黑树通过颜色来实现二叉搜索树的近似平衡

比起AVL树 红黑树的插入需要更少的旋转操作 所以说来经常增删的场景中红黑树更优

而由于它们查找的效率都很高 所以说AVL树优的查找的那一点优势并不算什么

所以说实际场景中红黑树用的更多一点

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

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

相关文章

远程监控网络摄像头通用指南

一、引言 随着物联网技术的发展&#xff0c;越来越多的场景需要我们通过技术手段去感知。画面和声音相当于机器的眼睛和耳朵&#xff0c;有了这些实时数据我们可以做很多事情&#xff0c;比如车牌识别、人脸识别、体温识别等等。本文将全方位介绍网络摄像头如何接入软件的实现…

2022.12 青少年机器人技术等级考试理论综合试卷(四级)

2022年12月 青少年机器人技术等级考试理论综合试卷&#xff08;四级&#xff09; 分数&#xff1a; 100 题数&#xff1a; 30 一、 单选题(共 20 题&#xff0c; 共 80 分) 1.以下关于 Arduino C 语言的说法&#xff0c; 正确的是?&#xff08; &#xff09; A.setup() 函数和…

SpringMVC Interceptor拦截器

SpringMVC中的拦截器用于拦截控制器方法的执行&#xff0c;执行在Controller前后&#xff0c;和视图渲染完成后。如下图所示&#xff1a; 一、创建拦截器 继承HandlerInterceptor 接口&#xff0c;并实现其中的方法 public class FirstInterceptor implements HandlerInter…

儿子小伟刚刚再婚,大衣哥就河南新乡商演,这是给孙子攒奶粉钱吗

现如今的社会&#xff0c;因为人们的攀比心理&#xff0c;结一次婚能让人脱一层皮&#xff0c;尤其是农村赚钱难&#xff0c;结婚花钱就更难了。其实不只是普通老百姓&#xff0c;强如农民歌唱家大衣哥这样的人&#xff0c;也架不住儿子一而再&#xff0c;再而三的结婚。 大衣哥…

Qt基础之二十一:QtRO(Qt Remote Object)实现进程间通信

这里将QtRO单独从上一篇Qt基础之二十:进程间通信拎出来,因为它是Qt5.9以后新加入的模块,专门用于进程间通信。其使用步骤有点类似之前介绍过的RPC(Remote Procedure Call)框架:gRPC和thrift,关于这两个框架详见 Qt中调用thrift和Qt中调用gRPC QtRO基于Socket封装,不仅支…

小程序开发——模板与配置

一、WXML 模板语法 1.数据绑定的基本原则 ① 在 data 中定义数据 ② 在 WXML 中使用数据2.在 data 中定义页面的数据 在页面对应的 .js 文件中&#xff0c;把数据定义到 data 对象中即可&#xff1a;3. Mustache 语法的格式 把data中的数据绑定到页面中渲染&#xff0c;使用…

【测试】java+selenium环境搭建

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、下载安装谷歌浏览器二、下载谷歌驱动三、常见问题&解决方法1. SessionNotCreatedException2. The version of ChromeDriver only support xxxxxxxxx3. The path to the driver executable the path to普通小…

5-2输入/输出管理-I/O核心子系统

文章目录一.I/O调度二.设备保护三.SPOOLing技术&#xff08;假脱机技术&#xff09;四.设备的分配与回收1.设备分配时应该考虑的因素2.静态分配和动态分配3.设备分配管理中的数据结构&#xff08;1&#xff09;设备控制表DCT&#xff08;Device Control Table&#xff09;&…

MySQL进阶篇之Linux安装MySQL8.0.26

Linux安装MySQL 需要更多安装MySQL的教程&#xff0c;请查阅Linux学习笔记——MySQL数据库管理系统安装部署 1、MySQL下载地址&#xff1a;https://downloads.mysql.com/archives/community/ 2、在FinalShell中输入rz&#xff0c;然后选择下载好的MySQL安装包&#xff0c;进行上…

【数据质量】一起聊聊数据质量

Garbage In, Garbage Out ​ 数据质量关注的是数据的健康&#xff0c;数据健康和人的健康很相似&#xff0c;人的健康会影响人的生活品质&#xff0c;同样数据的健康会影响数据的使用品质。为了保证我们健康&#xff0c;我们需要养成良好的生活习惯&#xff0c;膳食平衡&#x…

Open3D DBSCAN聚类(Python版本)

文章目录 一、简介二、算法步骤三、实现代码四、实现效果参考资料一、简介 DBSCAN算法,全称为“Density-Based Spatial Clustering of Applications with Node”,也就是“基于密度的聚类”。此类算法是假设聚类结构能通过样本分布的紧密程度确定,从样本密度的角度来考察样本…

亿发浅析:财务一体化功能与管理流程

在信息时代的背景下&#xff0c;企业信息化已成为中小企业降低成本、提高效率、提高竞争力的重要手段&#xff0c;也是中小企业实现长期可持续发展的有效途径。 信息化对企业管理的好处是显而易见的&#xff0c;如加快信息流&#xff0c;提高信息资源利用率&#xff0c;促进企业…

STM32使用FSMC驱动LCD

关于FSMC驱动LCD的函数LCD_WR_REG的理解首先你需要理解使用结构体LCD_BASE若有错误&#xff0c;请各位师兄师姐指点原理框图重要的函数理解关于LCD_BASE和函数LCD_WR_REG&#xff08;u16 regval&#xff09;的理解至于0X6C00 0802地址也是一样的。首先要说的是这是我个人的理解…

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 (五)位拆分与运算

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 &#xff08;五&#xff09;位拆分与运算 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN博客 &#x1f9e8;未经作者允许&#xff0c;禁止转载 &#x1f511;系列专栏&#xff1a;牛客Veri…

Burp Suite Professional 2023.1 (macOS, Linux, Windows) - Web 应用安全、测试和扫描

Burp Suite Professional, Test, find, and exploit vulnerabilities. 请访问原文链接&#xff1a;https://sysin.org/blog/burp-suite-pro-2023&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;www.sysin.org Burp Suite Professional&a…

《后端技术面试 38 讲》学习笔记 Day 07

《后端技术面试 38 讲》学习笔记 Day 07 21丨分布式架构&#xff1a;如何应对高并发的用户请求 原文摘抄 当同时访问系统的用户不断增加的时候&#xff0c;需要消耗的系统计算资源也不断增加&#xff0c;需要更多的 CPU 和内存去处理用户的计算请求&#xff0c;需要更多的网络…

二叉树、平衡二叉树、红黑树、B树、B+树、B*树的区别

二叉树 如下图&#xff0c;是一个二叉树的结构图片&#xff1a; 可以看到无论是对象“9”、还是“5”、“13”、“2”、“7”、“11”、“15”它们的下面分别都叉了两个其他的对象。而且这两个对象都是左边的数值要小一些&#xff0c;右边的数值要大一些。 所以这就是二叉树的…

Qt+C++堆叠多窗口界面切换

程序示例精选 QtC堆叠多窗口界面切换 如需安装运行环境或远程调试&#xff0c;见文章底部个人微信名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<QtC堆叠多窗口界面切换>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读…

停车系统源码-基于springboot+uniapp开源项目

Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统&#xff0c;支持封闭车场和路边车场&#xff0c;支持微信支付宝多种支付渠道&#xff0c;支持多种硬件&#xff0c;涵盖了停车场管理系统的所有基础功能。技术栈Springboot,Mybatis Plus,B…

聚观早报 | 推特将释放15亿用户名售卖;微信回应切断抖音外链

今日要闻&#xff1a;比亚迪摘得国内汽车年度销量冠军&#xff1b;推特将释放15亿用户名进行售卖&#xff1b;微信回应切断抖音外链&#xff1b;原阿里云盘负责人、Teambition 创始人齐俊元加入飞书&#xff1b;辉瑞CEO称新冠药不能太便宜推特将释放15亿用户名进行售卖 1 月 12…