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

目录

前言

一  红黑树简介

二  为什么需要红黑树

三  红黑树的特性

四  红黑树的操作

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://xiahunao.cn/news/3268074.html

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

相关文章

【YOLOv5/v7改进系列】引入中心化特征金字塔的EVC模块

一、导言 现有的特征金字塔方法过于关注层间特征交互而忽视了层内特征的调控。尽管有些方法尝试通过注意力机制或视觉变换器来学习紧凑的层内特征表示&#xff0c;但这些方法往往忽略了对密集预测任务非常重要的被忽视的角落区域。 为了解决这个问题&#xff0c;作者提出了CF…

力扣高频SQL 50题(基础版)第十七题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第十七题1075. 项目员工 I题目说明思路分析实现过程准备数据实现方式结果截图 力扣高频SQL 50题&#xff08;基础版&#xff09;第十七题 1075. 项目员工 I 题目说明 项目表 Project&#xff1a; ----------------…

uniapp引入自定义图标

目录 一、选择图标&#xff0c;加入购物车 二、下载到本地 三、导入项目 四、修改字体引用路径 五、开始使用 这里以扩展iconfont图标为例 官网&#xff1a;iconfont-阿里巴巴矢量图标库 一、选择图标&#xff0c;加入购物车 二、下载到本地 直接点击下载素材&#xff0…

为什么很多人在一定年龄后的肥胖无法避免

人体在营养均衡状态的时候&#xff0c;是不容易长胖的&#xff0c;且身体也远比一般人更健康些&#xff0c;但想要一直维持身体的这种健康均衡的状态&#xff0c;不仅生活上要很有规律&#xff0c;饮食上也要营养均衡才行。但以如今社会的快节奏生活而言&#xff0c;基本没有人…

Mindspore框架CRF条件随机场概率图模型实现文本序列命名实体标注|(三)双向LSTM+CRF模型构建实现

Mindspore框架CRF条件随机场概率图模型实现文本序列命名实体标注|&#xff08;一&#xff09;序列标注与条件随机场的关系 Mindspore框架CRF条件随机场概率图模型实现文本序列命名实体标注|&#xff08;二&#xff09;CRF模型构建 Mindspore框架CRF条件随机场概率图模型实现文本…

现代Java开发:使用jjwt实现JWT认证

前言 jjwt 库 是一个流行的 Java 库&#xff0c;用于创建和解析 JWT。我在学习spring security 的过程中看到了很多关于jwt的教程&#xff0c;其中最流行的就是使用jjwt实现jwt认证&#xff0c;但是教程之中依然使用的旧版的jjwt库&#xff0c;许多的类与方法已经标记弃用或者…

【Python机器学习】决策树的简单实践——预测隐形眼镜类型

使用小型数据集&#xff0c;我们就可以利用决策树学到很多知识&#xff1a;眼科医生是如何判断患者需要佩戴的镜片类型的&#xff1b;一旦理解了决策树的工作原理&#xff0c;我们甚至也可以帮助人们判断需要佩戴的镜片类型。 隐形眼镜数据集是非常著名的数据集&#xff0c;它包…

CSS常见属性详解——内边距与外边距

内边距与外边距 内边距 外边距 应用场景 在网页排版布局时&#xff0c;我们经常会希望元素与元素之间有一定的间距&#xff0c;此时我们可能会用到CSS的外边距或内边距属性&#xff0c;这两个属性都能让元素之间产生距离&#xff0c;那么他们之间有什么不同呢&#xff1f; …

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

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

深入探索PHP框架:Symfony框架全面解析

1. 引言 在现代Web开发领域&#xff0c;PHP作为一种广泛使用的服务器端脚本语言&#xff0c;其框架的选择对于项目的成功至关重要。PHP框架不仅能够提高开发效率&#xff0c;还能确保代码的质量和可维护性。本文将深入探讨Symfony框架&#xff0c;这是一个功能强大且灵活的PHP…

Python学习笔记45:游戏篇之外星人入侵(六)

前言 飞船模块的功能基本已经完成。今天继续完成子弹模块的功能。 子弹模块 子弹和飞船模块&#xff0c;在游戏逻辑中有一种生成与被生成的表面关系&#xff0c;因为子弹在游戏中是由飞船发射的。但是在我们实际抽象的过程中&#xff0c;飞船与子弹并不是is的关系&#xff0…

UML通信图建模技术及应用例

新书速览|《UML 2.5基础、建模与设计实践》 在对系统的动态行为进行建模时&#xff0c;通信图常被用于按组织结构对控制流进行建模。与顺序图一样&#xff0c;一个单独的通信图只能显示一个控制流。 使用通信图建模时可以遵循如下策略&#xff1a; &#xff08;1&#xff09…

普通人这一生逆袭的唯一机会

普通人这一生逆袭的唯一机会 在人生的长河中&#xff0c;每个普通人心中都藏着一个逆袭的梦想。梦想着从平凡走向卓越&#xff0c;从底层攀至顶峰。但梦想与现实之间&#xff0c;究竟有多远的距离&#xff1f;今天&#xff0c;让我们一起探索那些看似遥不可及&#xff0c;却又…

Unity UGUI 之 自动布局组件

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 本文在发布时间选用unity 2022.3.8稳定版本&#xff0c;请注意分别 1.什么是自动布局组件…

微服务注册中心

目录 1.微服务的注册中心 1.1 注册中⼼的主要作⽤ 1.2 常⻅的注册中⼼ 2.nacos简介 2.1 nacos实战⼊⻔ 2.2.1 搭建nacos环境 2.2.2 将商品微服务注册到nacos 3.服务调⽤Ribbon⼊⻔ 3.1 Ribbon概述 3.1.1 什么是Ribbon 3.1.2 Ribbon的主要作⽤ 3.2.2 ⼯程改造 4.服务…

openmv学习笔记(24电赛备赛笔记)

#openmv简介 openmv一种小型&#xff0c;可编程机器视觉摄像头&#xff0c;设计应用嵌入式应用和计算边缘&#xff0c;是图传模块&#xff0c;或者认为是一种&#xff0c;具有图像处理功能的单片机&#xff0c;提供多种接口&#xff08;I2C SPI UART CAN ADC DAC &#xff0…

204、【动态规划】牛客网 ——DP3 跳台阶扩展问题(Python版本)

题目描述 原题链接&#xff1a;DP3 跳台阶扩展问题 解题思路 一个DP问题&#xff0c;相比于普通爬楼&#xff08;只能爬一层或者两层&#xff09;对应的状态函数为 d p [ i ] d p [ i − 1 ] d p [ i − 2 ] dp[i] dp[i - 1] dp[i - 2] dp[i]dp[i−1]dp[i−2]。本题的dp…

vue3+g2plot实现词云图

词云图 效果预览: 核心代码: import {WordCloud } from @antv/g2plot;fetch(https://gw.alipayobjects.com/os/antfincdn/jPKbal7r9r/mock.json).then((res) => res.json()).then((data) => {const wordCloud = new WordCloud(container, {data,wordField: x,weigh…

秒懂Linux之权限

目录 一.Linux用户 二.文件权限 2.1 权限属性 chmod命令 chown与chgrp命令 2.2 文件类型 file指令 常见类型 2.3 常见权限问题 问题一&#xff1a; 问题二&#xff1a; 问题三&#xff1a; 一.Linux用户 Linux 下有两种用户&#xff1a;超级用户&#xff08; root …

kettle从入门到精通 第八十课 ETL之kettle kettle中的json对象字段写入postgresql中的json字段

场景&#xff1a;源数据库表为mysql的其中有json字段&#xff0c;通过kettle 查询出来 插入到目标数据库 postgresql中&#xff0c;对应的表中也有json字段。。但是报错&#xff0c;提示kettle查询出来是varchar的的字段&#xff0c;无法插入到目标数据库中。 1、创建测试表。 …