【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://www.xdnf.cn/news/1489152.html

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

相关文章

面试 SQL整理 常见的SQL面试题:大厂经典60题(一)

目录 SQL基础知识整理: 数据库基础知识 为什么要使用数据库 数据保存在内存 数据保存在文件 数据保存在数据库 什么是SQL&#xff1f; 什么是MySQL? 数据库三大范式是什么 mysql有关权限的表都有哪几个 MySQL的binlog有有几种录入格式&#xff1f;分别有什么区别&…

photoshop学习笔记——选区3 快速选择工具

快速选择工具 W shift W 在3种快速选择工具之间切换 对象选择工具 photoshop CC中没有这个工具&#xff0c;利用AI&#xff0c;将款选中的对象快速的提取选区&#xff0c;测试了一下&#xff0c;选区制作的非常nice快速选择工具 跟磁性套索类似&#xff0c;自动识别颜色相似…

24年第三届钉钉杯大学生大数据挑战赛浅析

需要完整资料&#xff0c;请关注WX&#xff1a;“小何数模”&#xff01; 本次钉钉杯大数据挑战赛的赛题已正式出炉&#xff0c;无论是赛题难度还是认可度&#xff0c;该比赛都是仅次于数模国赛的独一档&#xff0c;可以用于国赛前的练手训练。考虑到大家解题实属不易&#xf…

Laravel:揭秘PHP世界中最优雅的艺术品

1. 引言 在PHP的世界里&#xff0c;框架如繁星般璀璨&#xff0c;但Laravel以其独特的魅力和优雅&#xff0c;成为了众多开发者心中的艺术品。本文将深入探讨Laravel为何能在众多PHP框架中脱颖而出&#xff0c;成为最优雅的选择。 1.1 Laravel的诞生背景 Laravel的诞生可以…

React Native在移动端落地实践

在移动互联网产品迅猛发展的今天&#xff0c;技术的不断创新使得企业越来越注重降低成本、提升效率。为了在有限的开发资源下迅速推出高质量、用户体验好的产品&#xff0c;以实现公司发展&#xff0c;业界催生了许多移动端跨平台解决方案。这些方案不仅简化了开发流程&#xf…

代码随想录算法训练营day21 | 669. 修剪二叉搜索树、108.将有序数组转换为二叉搜索树、538.把二叉搜索树转换为累加树

碎碎念&#xff1a;加油 参考&#xff1a;代码随想录 669. 修剪二叉搜索树 题目链接 669. 修剪二叉搜索树 思想 递归三部曲&#xff1a; 参数和返回值&#xff1a;返回值返回的是处理好的二叉搜索树。终止条件&#xff1a;rootNULL 返回NULL&#xff1b;如果root的值小于…

【计算机网络】TCP三次握手和四次挥手

客户端–发送带有 SYN 标志的数据包–一次握手–服务端 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 为什么是三次握手而不是两次握手&#xff1f; 在不可靠的网络中&#xff0c;可能会出现包传输…

算法刷题day18|二叉树:669. 修剪二叉搜索树、108. 将有序数组转换为二叉搜索树、538. 把二叉搜索树转换为累加树

669. 修剪二叉搜索树 如果结点的值小于 low&#xff0c;那么说明该结点及它的左子树都不符合要求&#xff0c;我们返回对它的右结点进行修剪后的结果&#xff1b;如果结点的值大于 high&#xff0c;那么说明该结点及它的右子树都不符合要求&#xff0c;我们返回对它的左子树进…

2023IMO预选题几何第6题

锐角 △ A B C \triangle ABC △ABC 的外接圆为 ω \omega ω, 圆 I I I 与 ω \omega ω 内切于 A A A, 且与 B C BC BC 切于点 D D D. 设直线 A B AB AB, A C AC AC 分别与 I I I 交于点 P P P, Q Q Q, 点 M M M, N N N 在直线 B C BC BC 上, 满足 B B B 是 …

Python+selenium web自动化测试知识点合集2

选择元素 对于百度搜索页面&#xff0c;如果我们想自动化输入“selenium”&#xff0c;怎么做呢&#xff1f; 这就是在网页中&#xff0c;操控界面元素。 web界面自动化&#xff0c;要操控元素&#xff0c;首先需要 选择 界面元素 &#xff0c;或者说 定位 界面元素 就是 先…

基于豆瓣音乐、豆瓣图书、豆瓣电影详情获取、长短评获取【豆瓣全家桶系列】

文章目录 有需要本项目的代码或文档以及全部资源&#xff0c;或者部署调试可以私信博主 豆瓣电影系列基于Python的海量豆瓣电影、数据获取、数据预处理、数据分析、可视化、大屏设计项目&#xff08;含数据库&#xff09;基于多种机器学习的豆瓣电影评分预测与多维度可视化【可…

设计模式-结构型-09-外观模式

文章目录 1、影院管理项目2、外观模式基本介绍4、MyBatis 框架源码分析5、外观模式总结 1、影院管理项目 组建一个家庭影院&#xff1a; DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机&#xff0c;要求完成使用家庭影院的功能&#xff0c;其过程为&#xff1a; 直接用…

Docker安装oracle19c

文章目录 Docker安装oracle19c1. 拉取镜像2. 创建目录并赋权3. 构建容器并启动4. 查看日志5. 登录docker容器里面6. 登录sqlplus 创建PDB用户7. 查看show pdbs7. 切换数据库8. 创建用户9. 授权10. 使用navicat连接11. 参考和感谢 Docker安装oracle19c 1. 拉取镜像 docker pul…

Java集合——Array、ArrayList、LinkedList

1. ArrayList和Array的区别 1. 大小和自动扩容 Array&#xff1a;创建时指定大小&#xff0c;大小固定。若数组被创建&#xff0c;其大小不能更改 ArrayList&#xff1a;动态数组实现&#xff0c;可以动态增长或缩小。在不断添加元素时&#xff0c;ArrayList会自动进行扩容 …

3.4-GRU

1网络结构 1.1与LSTM相比 LSTM里面有三个门&#xff0c;还有一个增加信息的tanh单元&#xff0c;参数量相较于RNN显著增加&#xff1b; 因此GRU在参数上比LSTM要少&#xff1b; 另外&#xff0c;LSTM 将必要信息记录在记忆单元中&#xff0c;并基于记忆单元的信息计算隐藏状…

关键词查找【Knuth-Morris-Pratt (KMP) 算法】

一个视频让你彻底学懂KMP算法_哔哩哔哩_bilibili KMP算法的核心是利用匹配失败后的信息&#xff0c;尽量减少模式串与主串的匹配次数以达到快速匹配的目的。 第一步&#xff1a;计算模式串(子串)和next[j]数组 模式串 前2位字母的next[j]固定是0 和 1 后续字母的nex[j]&…

特斯拉财报看点:FSD拳打华为,Robotaxi 脚踢百度

大数据产业创新服务媒体 ——聚焦数据 改变商业 特斯拉发最新财报了&#xff0c;这不仅是一份财务报告&#xff0c;更是一张未来发展的蓝图。在这份蓝图中&#xff0c;两个关键词格外耀眼——FSD&#xff08;全自动驾驶系统&#xff09;和Robotaxi&#xff08;无人驾驶出租车&…

项目都做完了,领导要求国际化????--JAVA后端篇

springboot项目国际化相信各位小伙伴都会&#xff0c;很简单&#xff0c;但是怎么项目都做完了&#xff0c;领导却要求国际化文件就很头疼了 国际化的SpringBoot代码&#xff1a; 第一步&#xff1a;创建工具类 /*** 获取i18n资源文件** author bims*/ public class Message…

day08:订单状态定时处理、来单提醒和客户催单

文章目录 Spring Task介绍cron表达式入门案例 订单状态定时处理需求分析代码开发扩展 WebSocket介绍入门案例特点 来单提醒需求分析和设计代码实现 客户催单需求分析和设计代码实现 Spring Task 介绍 Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时…

20240725java的Controller、DAO、DO、Mapper、Service层、反射、AOP注解等内容的学习

在Java开发中&#xff0c;‌controller、‌dao、‌do、‌mapper等概念通常与MVC&#xff08;‌Model-View-Controller&#xff09;‌架构和分层设计相关。‌这些概念各自承担着不同的职责&#xff0c;‌共同协作以构建和运行一个应用程序。‌以下是这些概念的解释&#xff1a;‌…