【C++】——红黑树(手撕红黑树,彻底弄懂红黑树)

news2024/9/22 5:35:40

目录

前言

一  红黑树简介

二  为什么需要红黑树

三  红黑树的特性

四  红黑树的操作

4.1  变色操作

4.2  旋转操作

4.3 插入操作

4.4  红黑树插入代码实现

  4.5   红黑树的删除

五 红黑树迭代器实现

总结


前言

我们之前都学过ALV树,AVL树的本质就是一颗平衡二叉树,它的作用就是查找,插入和删除节点,最坏的时间复杂度都是O(logn)的,同时维护的高度差都是小于等于1的,但是也就是因为这个原因才被红黑树所替代

一  红黑树简介

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

二  为什么需要红黑树

对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。还有一点就是,AVL树需要大量的旋转,相比较红黑树来说效率有所减低

     

三  红黑树的特性

首先,红黑树也是一个二叉搜索树,也就是右边大,左边小,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡(最短路径就是全黑节点,最长路径就是一个红节点一个黑节点,当从根节点到叶子节点的路径上黑色节点相同时,最长路径刚好是最短路径的两倍。它同时满足以下特性:

1.根结点肯定树黑色的

2.不能出现连续的红色节点

3.从一节点到叶子节点到所有路径黑色节点的数量上相同的

4.节点的颜色顾名思义不是黑色就是红色

有了上面的认识,我们可以判断下面的图是不是红黑树,乍一看是没有违反规则的,但是实际上是这样吗?

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

可以看出他们的黑色结点数量并不相等,所以不是一颗红黑树

四  红黑树的操作

红黑树的基本操作和其他树形结构一样,一般都包括查找、插入、删除等操作。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树一样,比较简单,这里不再赘述。相对于查找操作,红黑树的插入和删除操作就要复杂的多。

对于红黑树的操作来说,因为和ALV树是有所区别的,所以旋转的操作是要少于ALV树的,那红黑树肯定就付出了其他的努力去替代这个操作,那就是变色,这也是红黑树的内核所在

4.1  变色操作

什么时候才需要变色呢?

当我们插入一个结点,造成有连续的红节点的时候,变色就是必不可少的了,之所以有连续的红是结点,是因为我们不能插入一个黑色结点,因为插入一个黑色结点会导致黑色结点的数量增多,使得另外的树无法去平衡这这棵树,所以插入黑色结点的代价要远比插入红色结点的代价要大得多

所以我们可以通过变色处理子树红色和黑色结点直接的位置关系,来达到子树本身的平衡



4.2  旋转操作

这里的旋转操作其实和ALV树的旋转是一样的,分为左旋右旋,左右双旋和右左双旋,但是我们这样旋转一般也需要用到变色操作,也就是旋转加变色操作使得红黑树平衡

如果有需要了解旋转的具体实现就看ALV树的旋转

4.3 插入操作

检测新节点插入后,红黑树的性质是否造到破坏?
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何
性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:


约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

情况一: cur为红,p为红,g为黑,u存在且为红 

这里向上调整导致,g的父亲结点变成下一个cur

这里可能就有疑问了,为什么单纯把p,u改为黑色,g变成红色??

1.g,p,u都为黑色

2.p 单独变黑色

这两种看起来没什么问题,但是对于1来说,如果这颗树为子树,那么就会多出来一个黑色结点,这是犯了大忌的,所以不可取。对于2来说,也是一样的道理。

所以这里采取的是p ,u 变黑色根节点变红,这样就维持了黑色结点的数量

如果 g 是根节点,那么直接变黑就行了

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

对于这种情况单纯的变色已经处理不了了,因为我们无论怎么变色处理,右子树都没有能力使得左右子树的黑色结点数量相同

这里我们的处理就是旋转加变色

1.p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,
2.p为g的右孩子,cur为p的右孩子,则进行左单旋转
3.p、g变色--p变黑,g变红

注意:这里无论u结点是否存在,都进行一样的操作,进行操作过后都会使得左右子树的黑色结点数量相同,因为在上面的情况讨论中,如果u结点是黑色,那么cur一定是更新上来的,所以cur下面肯定是有黑色结点保持平衡的,所以这里的旋转过后也是平衡的,如果u不存在,那么旋转加变色也是没有任何问题的,可以自己画图模拟一遍

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

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,
p为g的右孩子,cur为p的左孩子,则针对p做右单旋转
则转换成了情况2后面就按 情况2 的来就行了

4.4  红黑树插入代码实现

我们要插入得先找到插入的位置在哪

bool insert(const T& data)
	{
		if (_root == nullptr)//如果头节点为空,直接插到头节点上
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		Node* parent = nullptr;//如果不是空,那么设置一个当前结点和一个父亲结点去寻找插入的位置
		Node* cur = _root;

		while (cur)
		{
			if (cur->_data < data)//这里和二叉搜索树的情况一样
			{
				parent = cur;
				cur = cur->_right;
			}
			else if(cur->_data>data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;找到了那么说明存在一个相同的结点,那么直接返回false;
			}
		}

		cur = new Node(data);//走到这里说明找到的位置合适在这里进行插入
		Node* newnode = cur;
		cur->_col = RED;//颜色设置为红色
        //这里还需要判断是在父亲的右还是左,把它和它的父亲结点链接上
        if (parent->_data < data)
		{
			parent->right = cur;
			cur->_parent = parnet;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

插入结点后,我们就要开始维护红黑树的结构了,这里我们按照上面的情况一 一去模拟就行

1.首先我们需要判断的是父亲结点是否存在,如果存在那它的颜色是什么,从这里开始判断后面的操作,如果不存在或者是黑色那么我们就不用去调整了,因为我们插入的结点是红色

2.如果需要去调整,那我们就应该设置一个祖宗结点,有便于向上调整。

3.判断叔叔结点是否存在且颜色为红还是黑,这关乎到了如果进行调整

4.如果叔叔结点不存在,如果父亲结点在祖宗结点的左边,那么对祖宗结点进行右单旋加变色处理

如果在右边,则相反

5.如果叔叔存在且为黑,和上面一样的判断,在左边,对祖宗结点进行一个右单旋加变色,在右边则对父亲结点进行左单旋,然后再对祖宗结点进行右单旋

6.如果叔叔存在且为红,那么变色就行,把父亲和叔叔结点变为黑色,把祖宗结点变为红色,这样就保持了黑色结点的平衡,如果这里把祖宗结点变为黑色,如果是根节点还可以,如果不是根节点那么就不行,因为多了一个黑色结点

while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				
				if (uncle && uncle->_col == RED)//如果为红色且存在
				{
					parent->_col = uncle->col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else//如果不存在和黑色的处理是一样的,上面的情况讨论有说明
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_clo = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else if (parent == grandfather->_right)//这里就是反过来
			{
				Node* uncle = grandfather->_left;

				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur = parent->_right)
					{
						RoateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RoateR(parent);
						RoateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;


	}

  4.5   红黑树的删除

这里红黑树的删除其实和搜索二叉树那里的删除是差不多的思路,只不过加了红黑树的调整,如果对于搜索二叉树的删除过程忘记了可以参考搜索二叉树详解这篇博客

对于ALV树来说,删除和插入对比起来复杂太多,红黑树就更不用说了,删除一个结点,删除完毕以后还要去调整变色,删除叶子结点还行,如果是删除中间结点那么就会变得异常复杂,想到这里我就不想进行下去了😂😂,了解了解就行,看着都恐怖

五 红黑树迭代器实现

对于树形结构的迭代器来说相比于其他迭代器是要复杂一些的,因为不再是链式结构那样无脑遍历了

首先我们遍历二叉树是采取的中序遍历(左子树,根,右子树),所以我们得先想清楚遍历情况

我们在进行迭代器的++的时候,考虑的是该结点的是否存在右子树,因为我们位于一个结点上,说明左子树已经遍历完了,如果说右子树存在,那么就去找右子树的最左结点

如果右子树不存在,再++则需要看这课子树是不是遍历完了,如果当前结点的父亲结点右指针是当前结点,说明这个子树完了,则需要往上调整,继续判断这种情况

这张图很显然在根的左子树上,可以看到现在以1为结点的子树遍历完了,所以应该遍历根,再然后进入右子树找最左结点继续上面的遍历

如果上面不是很理解那么就看代码理解一遍

	Self& operator++()
	{
		if (_node->_right)
		{
			// 下一个就是右子树的最左节点
			Node* cur = _node->_right;
			while (cur->_left)
			{
				cur = cur->_left;
			}

			_node = cur;
		}
		else
		{
			// 左子树 根 右子树
			// 右为空,找孩子是父亲左的那个祖先
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}

			_node = parent;//这个时候就是第二张图,此时_node应该指向的是父亲结点,因为下一个
                           //遍历的结点是根结点
		} 

		return *this;
	}

那我们在进行--操作其实代码和++是一样的, 这里可以想一下,我们++是怎么操作的,比如我们++要找下一个结点,那么就会找右子树的最左结点,其实最后找到的是右子子树的最右结点

比如这里的27号结点,当我们再++就往回退了。

那现在我们看这张图,6结点++以后到1,再++到8,那么我们--就需要到1,那么就和++是一样的,先判断该节点的左子树是否存在,然后找左子树的最右结点,然后退无可退的时候就往回返了

	Self& operator--()
	{

		if (_node->_left)
		{
			//下一个是左子树的最右节点
			Node* right = _node->_left;
			while (right->_right)
			{
				right = right->_right;
			}
			_node = right;
		}
		else
		{
			//孩子不是父亲的左的那个祖先
			Node* parent = _node->_parent;
			Node* cur = _node;
			//如果是父亲的右子树,就一直往上走
			//如果parent已经为空,那么就停止循环,parent已经到达了我们的end的位置
			while (parent && cur == parent->_left)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}

			_node = parent;
		}
		return *this;
	}

总结:这里可能比较绕,时间久了不记得了,我们只需要知道++的时候是往右边走,也就是找右子树的最左节点,--的时候往左边走,找左子树的最右结点,如果到底了就往回返

这里开始的begin()就是最左结点,end是空,因为我们一直++一定会返回到根,根的父亲为空

总结

红黑树这里其实插入操作也不是很难,迭代器有点绕,多结合图去理解代码!!!

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

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

相关文章

Oracle对比两表数据的不一致

MINUS 基本语法如下 [SQL 语句 1] MINUS [SQL 语句 2];举个例子&#xff1a; select 1 from dual minus select 2 from dual--运行结果 1-------------------------------- select 2 from dual minus select 1 from dual--运行结果 2所以&#xff0c;如果想找所有不一致的&a…

软件测试---Linux

Linux命令使用&#xff1a;为了将来工作中与服务器设备进行交互而准备的技能&#xff08;远程连接/命令的使用&#xff09;数据库的使用&#xff1a;MySQL&#xff0c;除了查询动作需要重点掌握以外&#xff0c;其他操作了解即可什么是虚拟机 通过虚拟化技术&#xff0c;在电脑…

富芮坤FR800X系列之按键检测模块设计

FR800X系列按键检测模块 读者对象&#xff1a; 本文档主要适用以下工程师&#xff1a; 嵌入式系统工程师 单片机软件工程师 IOT固件工程师 BLE固件工程师 文章目录 1.概要2.用户如何设计按键检测模块2.1 GPIO初始化2.2按键模块初始化2.3设计中断函数&#xff1a;2.4循环…

【Python面试题收录】Python编程基础练习题①(数据类型+函数+文件操作)

本文所有代码打包在Gitee仓库中https://gitee.com/wx114/Python-Interview-Questions 一、数据类型 第一题&#xff08;str&#xff09; 请编写一个Python程序&#xff0c;完成以下任务&#xff1a; 去除字符串开头和结尾的空格。使用逗号&#xff08;","&#…

【数据库】Quartz2.3 框架 数据库设计说明书

1、 Quartz表说明 2、 quartz 的触发时间的配置 1、 cron 方式&#xff1a;采用cronExpression表达式配置时间。 2、 simple 方式&#xff1a;和JavaTimer差不多&#xff0c;可以指定一个开始时间和结束时间外加一个循环时间。 3、 calendars 方式&#xff1a;可以和cron配合使…

Java-----栈

目录 1.栈&#xff08;Stack&#xff09; 1.1概念 1.2栈的使用 1.3栈的模拟实现 1.4栈的应用场景 1.5栈、虚拟机栈、栈帧有什么区别呢 1.栈&#xff08;Stack&#xff09; 1.1概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操…

Centos8 yum 更换源以及安装内核头文件

文章目录 一、简介二、yum 更换源三、安装内核头文件 一、简介 CentOS 是一个开源项目&#xff0c;发布了两个不同的 Linux 发行版——CentOS Stream 和 CentOS Linux 。 CentOS Stream 是即将发布的红帽企业 Linux 产品的上游开发平台。 CentOS 项目将于 2024 年 6 月 30 日…

场外期权如何报价?名义本金是什么?

今天带你了解场外期权如何报价&#xff1f;名义本金是什么&#xff1f;投资者首先需要挑选自己想要进行期权交易的沪深上市公司股票。选出股票后&#xff0c;需要将股票信息、预期的操作时间&#xff08;如期限&#xff09;、看涨或看跌的选择以及预计的交易金额等信息报给场外…

商家虚假发货行为频发,电商平台如何通过物流轨迹来监管?(内附视频号、抖音、京东的发货规则)

近年来&#xff0c;“虚假发货”问题在电商行业中日益凸显。某投诉平台数据显示&#xff0c;截至2024年7月&#xff0c;搜索“虚假发货”显示的投诉高达19万条&#xff0c;如何有效监控卖家发货的合规性与及时性、打击虚假发货行为成为电商平台的重要议题。 为了维护消费者权益…

剧透:巴黎奥运会用上了AI转播

** AI增强技术&#xff0c;让比赛画面变成电影特效。 ** 巴黎奥运会即将开幕&#xff01; 阿里云在奥运转播中应用的AI增强技术 将让比赛画面变成电影特效&#xff01; 剧透如下 &#x1f447;&#x1f3fb; 阿里云为奥运转播提供的高自由度回放“子弹时间”&#xff0c;是…

[Mysql-DDL数据操作语句]

目录 DDL语句操作数据库 库&#xff1a; 查看&#xff1a;show 创建&#xff1a;creat 删除&#xff1a;drop 使用(切换)&#xff1a;use 表&#xff1a; 查看&#xff1a;desc show 创建&#xff1a;create 表结构修改 rename as add drop modify change rename as …

cesium海洋到站提示

项目地址:Every Admin: 用于快速搭建后台管理和其他页面的项目,组件化开发,以及大屏展示. <template> <div class"topbox"> xx海洋管理 </div> <div class"selectbox"> <div class"title"> 航线列表 </div>…

了解Java虚拟机(JVM)

前言&#x1f440;~ 上一章我们介绍网络原理相关的知识点&#xff0c;今天我们浅浅来了解一下java虚拟机JVM JVM&#xff08; Java Virtual Machine &#xff09; JVM内存区域划分 方法区/元数据区&#xff08;线程共享&#xff09; 堆&#xff08;线程共享&#xff09; 虚…

Nginx 配置与优化:常见问题全面解析

文章目录 Nginx 配置与优化:常见问题全面解析一、Nginx 安装与配置问题1.1 Nginx 安装失败问题描述解决方法1.2 Nginx 配置文件语法错误问题描述解决方法二、Nginx 服务启动与停止问题2.1 Nginx 无法启动问题描述解决方法2.2 Nginx 服务无法停止问题描述解决方法三、Nginx 性能…

尚硅谷vue全家桶(vue2+vue3)笔记

Vue2 一、Vue核心 01_简介 1.特点 采用组件化模式&#xff0c;提高代码复用率、且让代码更好维护。声明式编码&#xff0c;让编程人员无需直接操作DOM&#xff08;命令式编码&#xff09;&#xff0c;提高开发效率。使用虚拟DOM优秀的Diff算法&#xff0c;尽量复用DOM节点。…

【日常记录】【JS】JS中查询参数处理工具URLSearchParams

文章目录 1. 引言2. URLSearchParams2.1 URLSearchParams 的构造函数2.2 append() 方法2.3 delete() 方法2.4 entries() 方法2.5 forEach() 方法2.6 get() 方法2.7 getAll() 方法2.8 has() 方法2.9 keys() 方法2.10 set() 方法2.11 toString() 方法2.12 values() 方法 参考链接…

Pytorch深度学习实践(5)逻辑回归

逻辑回归 逻辑回归主要是解决分类问题 回归任务&#xff1a;结果是一个连续的实数分类任务&#xff1a;结果是一个离散的值 分类任务不能直接使用回归去预测&#xff0c;比如在手写识别中&#xff08;识别手写 0 − − 9 0 -- 9 0−−9&#xff09;&#xff0c;因为各个类别…

python毕业设计选题协同过滤算法在音乐推荐系统

✌网站介绍&#xff1a;✌10年项目辅导经验、专注于计算机技术领域学生项目实战辅导。 ✌服务范围&#xff1a;Java(SpringBoo/SSM)、Python、PHP、Nodejs、爬虫、数据可视化、小程序、安卓app、大数据等设计与开发。 ✌服务内容&#xff1a;免费功能设计、免费提供开题答辩P…

【进程检测】使用pywin32捕获window进程信息

需求 检测win系统依赖服务进程的运行情况&#xff0c;版本信息&#xff08;进程检测器&#xff09;检测内外网连接情况 实现 进程检测 # 使用pywin32获取进程版本信息 def get_version_info(path):try:info GetFileVersionInfo(path, \\)ms info[FileVersionMS]ls info[…

基于单片机控制的气动机械手设计

摘 要&#xff1a; 机械手拥有灵活的运动结构&#xff0c;可以在控制系统控制下完成复杂的运动&#xff0c;从而实现高效率的自动化生产方式&#xff0c;因而成为发展工业生产技术的重要方向。气动技术和单片机技术已相当成熟&#xff0c;工业应用广泛&#xff0c;该文将基于单…