【C++笔记】红黑树的简易实现

【C++笔记】红黑树的简易实现

  • 一、什么是红黑树以及红黑树好在哪里
    • 1.1、什么是红黑树
    • 1.2、红黑树比AVL树好在哪里?
  • 二、红黑树的模拟实现
    • 2.1、红黑树的插入
    • 2.2、仅变色调整
    • 2.3、变色+单旋调整
    • 2.4、变色+双旋调整

一、什么是红黑树以及红黑树好在哪里

1.1、什么是红黑树

红黑树本质上也是一颗搜索二叉树,但它在搜索二叉树的规则上有新添了一些额外的规则,使得它比普通的搜索二叉树甚至是AVL树的性能更好。

简单来说红黑树是这样一棵树:红黑树是一棵搜索二叉树,它的每个节点上增加了一个存储位来表示每个节点的颜色,颜色要么是红色要么是黑色。并且树中没有连续的红色节点,且红黑树中确保没有任何一条路径长度会大于其他路径的两倍。

红黑树有以下几个主要特性:

1. 每个结点不是红色就是黑色
2. 根节点是黑色的
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的(即没有连续的红节点)
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点

大家可能看完会感到疑惑,上面的特性中好像并没有与路径长度相关的啊,那怎么保证上面说到的确保没有任何一条路径长度会大于其他路径的两倍呢?

其实这个条件我们并不需要特意的去关注,因为只要满足了特性3和特性4,这个条件自然就满足了。

我们先来看特性4中所说的任意路径的黑色节点数量都一样,这个其实已经在一定程度上限制了每条路径的长度了,因为如果某棵红黑树中都是黑色节点的话,那么每一条路径的长度都是一样的,比如说下面这棵树:
在这里插入图片描述
想想就知道,上面这棵树一定是满足红黑树的条件的。

那么在每条路径的黑节点数量都相同的情况下,要加入红节点来使路径变长,应该怎么加才能使路径最长呢?

由于特性3的限制,即不能有连续的红节点,并且根节点一定是黑节点,我们要使一条路径最长的唯一添加方式就只有像下面这样:
在这里插入图片描述
只能这样一直红黑相间的加,并且到最后红黑节点的数量一定是相同的,即最长的路径是每条路径黑色节点的两倍。
而每条路径的黑色节点数量都是相同的,所以若是存在最短路径(这里的最短路径不是针对某一棵红黑树,而是在合法范围内能达到的最短路径),那么这条路径上的节点一定是全黑的。

所以这也就满足了最长路径不超过最短路径的二倍的条件了。

最后还有一点是需要特别注意的: 红黑树的路径定义其实和其他树的路径的定义不一样,其他树的路径定义都是由根节点到叶子结点,而红黑树的路径定义则是由根节点到空。

我这里先说原因,也就是为什么需要这样,然后再说如果不这样的话会导致的一个问题。

原因:
因为红黑树并不像AVL树一样使用一个平衡因子来控制整棵树的高度,所以如果红黑树的路径定义和其他树一样就会出现下面这种极端情况:
在这里插入图片描述
如果路径定义的时由根节点到叶子结点,那么这棵树的高度就有可能向上面一样变得很高,但是节点的数量并不多,并且只要路径中的黑色节点相同,它有可能一直延伸下去:
在这里插入图片描述
如果树的高度变得很高,我们的查找效率就会变低,这时候如果将路径定义成由根节点到空的话就能很好的规避这种情况了:
在这里插入图片描述
这样的话,上图红线标出来的这条路径就不合法了,就需要调整。

同时这样定义其实也是怕我们判断错误,如果路径的定义还是像其他二叉树一样,那我们就会认为上图的这棵树并没有出错的地方,就判断错了。

1.2、红黑树比AVL树好在哪里?

但是红黑树到底比AVL树好在哪里呢?

我们知道AVL树是是通过平衡因子来控制右子树与左子树的高度差不超过1的,既然高度差不超过1,那说明AVL树是一棵近似完全二叉树甚至是满二叉树的树,是一个严格平衡的树:
在这里插入图片描述
虽然它的高度控制的很好,但是我们在控制高度的时候需要进行各种旋转操作,在旋转的过程中还需要进行很多的判断,这相对于查找的判断确实多了很多。

因为AVL树对于高度的严格控制,在一些极端情况下我们可能没插入一个节点就需要旋转一次,这其实是很费时间的。

而红黑树对于高度的控制没有AVL树一样严格,这其实就省去了很多旋转的开销,虽然它的高度差较与AVL树更大,但也只是查找的层数多一点而已,相对于AVL树需要进行繁琐的旋转,旋转中还有很多的判断,显然查找更多层其实也并不赖。

所以总体而言,红黑树较于AVL树还是有一点优势的,虽然优势并不是很大,但好过没有。

二、红黑树的模拟实现

红黑树节点:

// 颜色我们用枚举来定义,比较形象一点
enum Color {RED,BLACK
};// 红黑树节点
template <class T>
struct RBTreeNode {RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;T _val;Color _col;// 构造RBTreeNode(const T& val):_left(nullptr), _right(nullptr), _parent(nullptr), _val(val), _col(RED) // 新增节点要给红色{}
};

因为红黑树也是要旋转的,所以和AVL树一样,也需要用到三叉链。

然后我们同样只需要在红黑树类里面封装一个根节点即可:

template <class T>
calss RBTree{
public :typedef RBTreeNode<T> Node;// …………
private :Node* _root;}

因为红黑树的删除操作实在太过繁琐,而且也在考察范围,所以我们这里就只实现红黑树的插入操作即可。

2.1、红黑树的插入

红黑树的本质是一颗二叉排序树,所以和普通的二叉排序树一样,还是需要先找到插入的位置。

但是,红黑树有红色和黑色两种颜色,我们在插入新节点的时候,新节点是选择红色还是黑色呢?

其实这个问题好好想一下就知道了,因为红黑树最主要的特性就是每条路径的黑色节点数量相同,如果新增节点选择黑色,那么这就会导致有一条路径的黑色节点数量不同了,这影响到的棵树整棵树啊!这样的话,需要再次调整的话就需要对整棵树进行调整了,非常麻烦。

所以我们新增的节点一律选择红色,如果后面需要调整的话,调整的幅度也不会太大。

先给出查找插入位置和插入逻辑的代码,这和普通的搜索二叉树一样:

// 插入
bool insert(const T& val) {if (nullptr == _root) {_root = new Node(val);// 根节点一定要给黑色_root->_col = BLACK;cout << "insert->" << val << endl;return true;}// 先插入新节点Node* cur = _root;Node* parent = nullptr;while (cur) {if (val < cur->_val) {parent = cur;cur = cur->_left;}else if (val > cur->_val) {parent = cur;cur = cur->_right;}else {return false;}}cur = new Node(val);if (val < parent->_val) {parent->_left = cur;}else {parent->_right = cur;}cur->_parent = parent;
}

在插入新节点后我们还需要检查一下是否需要进行调整,也就是检查一下红黑树的结构是否被破坏了。

有一种情况是最轻松的,也就是结构没有被破坏,当我们插入的新节点的父亲是黑色的时候:
在这里插入图片描述
这时候其实并不用管,新节点插入在父亲的左边还是右边,因为都是一样的,我们直接返回即可。

// 检查是否需要调整Node* grandfather = parent->_parent;Node* uncle = nullptr;if (parent->_col == BLACK) {// 如果父亲是黑色,则不用调整,直接返回return true;}

2.2、仅变色调整

还有些情况是需要进行变色或旋转调整的,当新插入节点的父亲节点是红色的时候,我们是无论如何都需要调整的,但有些调整比较轻松,有些调整就比较复杂,有可能是只需要变色即可,有可能是需要变色加上旋转调整。

我们这里还是先从轻松地说起,即仅需要变色的情况。
在这里插入图片描述
如上图,当新节点的父亲是红色,且叔叔节点(uncle)即父亲的兄弟节点“存在”且为红的时候,grandfather为黑(此时的grandfather已定位黑),我们就仅需要变色处理即可。
具体的变色方案是:

将parent和uncle都变黑,将grandfather变红

在这里插入图片描述
这时候

但是我们还不能直接结束,因为上面所画的只是一个情况,也有可能当我们将grandfather变红之后发现grandfather的父亲也是一个红节点(因为当前的祖父节点不一定是根),这时候就有违反规则了。

所以我们还需要继续向上调整,我们画出个抽象图可能更好理解:
在这里插入图片描述
如上图,当cur的父亲节点§为红色,并且父亲节§点和叔叔节点(u)都为红色时,就需要进行上述变色调整,调整完当前层的时候还需要检查上一层。

只需要将g当成新的cur,然后一直迭代的进行判断。

但是这样调整为什么就能解决问题了呢?
这是因为,节点g其实是p和u所在的这两条路径“共用”的:
在这里插入图片描述
所以这样调整后看似是减少了两个黑色节点只增加了一个黑色节点,好像是少了一个黑色节点,但是从路径中的黑色节点数量来看,其实是并没有减少的。

注意:再向上层迭代的过程中,也有可能出现其他的情况,有可能他还是只需要变色,有可能是需要变色+旋转调整,所以还需要结合下面讲到的调整方式相结合。

2.3、变色+单旋调整

cur为红,p为红,g为黑,u不存在/u存在且为黑时,我们就不仅仅需要变色了,还需要进行旋转调整,具体是单旋还是双旋还需要看情况。

首先先把原理讲清楚,就是为什么这样的情况需要旋转。
首先当u节点不存在时,此时的cur必定是新增节点:
在这里插入图片描述
并且p节点在增加cur节点之前也一定是没有孩子的,因为p已经是红色了,而红节点的孩子必须是黑节点,所以如果p有孩子节点,就破坏了红黑树的规则了。

此时无论怎样变色都不不能解决问题的,因为现在的最长路径已经超过了最短路径的两倍了,只能通过调整高度来解决问题了。

如果p是g的左孩子并且cur也是p的左孩子就要进行右单旋:
在这里插入图片描述

调整完后,变色的方案是,p变黑g变红。

而如果p是g的右孩子且cur是p的右孩子,就要进行左单旋。

但为什么当u节点存在且为黑的时候也需要进行旋转呢?
首先可以确定的是,当存在且为黑的时候,cur一定不是新增节点,因为如果cur实现增节点的话,那在新增cur之前整棵树就不符合红黑树的规则了:
在这里插入图片描述
所以cur原来一定是黑节点,是下层调整上来的时候变红的,本质是少了一个黑节点。
并且子树abcde也一定是不为空的,不然就不符合规则了:

在这里插入图片描述

那我们就来细细分析一下,首先cur是从下面调整上来变成的红色,所以cur这可子树一定是符合规则的,所以cur就不能再变色了,再变色的话不就是走回头路了吗。
同时p节点也是不能变色的,因为如果p变为黑色,看似是不上了之前少了的那个黑节点,但是子树c的每条路径上又多出了一个黑节点,也是不符合要求了。

在这里插入图片描述
同时u更是不能变色的啦,及解决不了问题还会使g的右子树都少了一个黑色节点,同理g也不能变色。

所以我们就只能通过旋转来解决问题了:
在这里插入图片描述
然后我们观察发现,ab子树所在的路径都少了一个黑节点,c子树的黑色节点是不变的,所以变色方案也是和上面一样:p变黑,g变红:
在这里插入图片描述
同样左单选的情况也是类似的分析。

先给出左单旋和右单选的代码:

// 左单旋
void RotateL(Node* parent) {Node* subRight = parent->_right;Node* subRightL = subRight->_left;Node* parent_parent = parent->_parent;parent->_right = subRightL;if (subRightL) {subRightL->_parent = parent;}subRight->_left = parent;parent->_parent = subRight;if (parent == _root) { // 如果当前的parent是根_root = subRight;subRight->_parent = nullptr;}else {// 如果当前的parent还不是根if (parent == parent_parent->_left) {parent_parent->_left = subRight;}else {parent_parent->_right = subRight;}subRight->_parent = parent_parent;}}// 右单旋
void RotateR(Node* parent) {Node* subLeft = parent->_left;Node* subLeftR = subLeft->_right;Node* parent_parent = parent->_parent;parent->_left = subLeftR;if (subLeftR) {subLeftR->_parent = parent;}subLeft->_right = parent;parent->_parent = subLeft;if (parent == _root) { // 如果当前的parent为根_root = subLeft;subLeft->_parent = nullptr;}else {// 如果当前的parent不为根if (parent == parent_parent->_left) {parent_parent->_left = subLeft;}else {parent_parent->_right = subLeft;}subLeft->_parent = parent_parent;}
}

其实就是复制AVL树的左单旋和右单旋。

2.4、变色+双旋调整

然后就到了双旋的情况了,其实红黑树的双旋和AVL树的双旋是差不多的,只是多了变色这一步而已。
如下图,当p为g的左孩子并且cur为p的右孩子时,我们使用单旋是解决不了问题的:
在这里插入图片描述
此时我们可以像AVL树一样先对p进行左单旋,将其转化成一个单纯的右单旋的情况:
在这里插入图片描述
此时我们将cur和p调换位置,不就变成了和上面单旋处理一样的情况了吗?
在这里插入图片描述
所以我们再针对g做一次右单旋不就解决高度不平衡的问题了吗?
在这里插入图片描述

完整过程:

在这里插入图片描述
此时的变色方案就和单选的有所不同了,是将cur变为黑色,g变为红色:
在这里插入图片描述
为什么不同其实也很好理解:


如上图,我们在对g进行左单旋后是变成了上图左端的形式,我们其实是将p“看作”是cur来对g进行单旋的,所以我们可以认为是p和cur调换了位置。

所以在单选和双旋中,g都变成了红色,但是变黑色的却分别是p和cur,带着这样的理解我们其实会发现单选和双旋其实本质也是一样的。

最后给出完整的插入代码:

// 插入
bool insert(const T& val) {if (nullptr == _root) {_root = new Node(val);// 根节点一定要给黑色_root->_col = BLACK;cout << "insert->" << val << endl;return true;}// 先插入新节点Node* cur = _root;Node* parent = nullptr;while (cur) {if (val < cur->_val) {parent = cur;cur = cur->_left;}else if (val > cur->_val) {parent = cur;cur = cur->_right;}else {return false;}}cur = new Node(val);if (val < parent->_val) {parent->_left = cur;}else {parent->_right = cur;}cur->_parent = parent;// 检查是否需要调整Node* grandfather = parent->_parent;Node* uncle = nullptr;if (parent->_col == BLACK) {// 如果父亲是黑色,则不用调整,直接返回return true;}else {while (parent && parent->_col == RED) {grandfather = parent->_parent;if (parent == grandfather->_left) {uncle = grandfather->_right;}else {uncle = grandfather->_left;}// 如果uncle存在且为红if (uncle && uncle->_col == RED) {parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;parent = cur->_parent;}else { // 如果uncle不存在或者存在且为黑if (parent == grandfather->_left) {if (cur == parent->_left) { // 右单旋RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else { // 左右双旋RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}}else {if (cur == parent->_right) { // 左单旋RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else { // 右左双旋RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}}break;}}}// 再次处理,根一定要是黑色_root->_col = BLACK;return true;
}

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

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

相关文章

Docker容器网络模式

1.none网络 1&#xff09;使用默认网络模式创建一个BusyBox容器&#xff0c;用于对比none网络模式&#xff1b; 测试网络&#xff0c;可以正常连接外网。 2&#xff09;再创建一个none网络模式的BusyBox容器&#xff1b; 测试网络连接&#xff0c;无法连接外网。 总结&#x…

深入了解Spring Boot中@Async注解的8大坑点

文章目录 1. 缺少EnableAsync注解2. 异步方法需独立3. 不同的异步方法间无法相互调用4. 返回值为void的异步方法无法捕获异常5. 外部无法直接调用带有Async注解的方法6. Async方法不适用于private方法7. 缺失异步线程池配置8. 异步方法与事务的兼容结语 &#x1f389;深入了解S…

Ros报错:The Plugin for class ‘jsk_rviz_plugin/Plotter2D‘ failed to load

一般出现这种情况&#xff0c;是提醒Ros缺少某种库&#xff1a; 图中显示的错误是说明少了jsk_rviz_plugins库&#xff0c;他是一个提供原始rviz插件的包。 解决办法是安装相应的库与插件&#xff1a; #根据自己ROS的版本选择相应的指令 # ubuntu20.04:noetic sudo apt-get i…

RabbitMQ消息的应答

消息的应答机制 消费者完成一个任务可能需要一段时间&#xff0c;如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了&#xff0c;会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息&#xff0c;便立即将该消息标记为删除。在这种情况下&#xff0c;突然有个…

计算机视觉:使用dlib实现人脸检测

1 dlib介绍 Dlib是一个广泛使用的开源库&#xff0c;在计算机视觉和机器学习领域具有重要影响。它是由Davis King在2002年开发&#xff0c;主要用C语言编写&#xff0c;但也提供了Python接口。Dlib结合了高效的算法和易用性&#xff0c;使其成为学术界和工业界的热门选择。 1.…

SpringBoot项目启动后自动停止了?

1 现象 2023-11-22T09:05:13.36108:00 DEBUG 17521 --- [ main] o.s.b.a.ApplicationAvailabilityBean : Application availability state LivenessState changed to CORRECT 2023-11-22T09:05:13.36208:00 DEBUG 17521 --- [ main] o.s.b.a.Applicat…

一文1000字彻底搞懂Web测试与App测试的区别

总结分享一些项目需要结合Web测试和App测试的工作经验给大家&#xff1a; 从功能测试区分&#xff0c;Web测试与App测试在测试用例设计和测试流程上没什么区别。 而两者的主要区别体现在如下几个方面&#xff1a; 1 系统结构方面 Web项目&#xff0c;B/S架构&#xff0c;基…

Android中实现RecyclerView,并对item及其多个子控件的点击事件监听

目录 背景 实现RecyclerView 第一步、 新建item的xml 第二步、在activity的布局中引入 RecyclerView 第三步、新建一个adapter 第四步、在activity中初始化绑定adapter即可 实现item及其多个子组件点击事件监听 第一步、 适配器中创建监听对象 第二步、适配器中绑定监听…

uniapp ios 授权弹窗 uniapp弹出框怎么实现

新版本的信息弹窗组件 可以弹出很多条信息&#xff0c;并单独控制消失时间、点击消失。 用循环来生成很多个弹窗&#xff0c;用this.$refs来传值&#xff0c;并添加数组。 1.布局 2.js 具体流程。需要一个弹窗&#xff0c;基本信息传入组件&#xff0c;处理后添加入数组&am…

智能优化算法应用:基于闪电搜索算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于闪电搜索算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于闪电搜索算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.闪电搜索算法4.实验参数设定5.算法结果6.参考…

【C++】异常处理 ① ( 异常概念引入 | 抛出异常语法 | 捕获异常语法 | 异常捕获流程 | 异常处理代码示例 )

文章目录 一、异常处理1、异常概念引入2、抛出异常语法3、捕获异常语法4、异常捕获流程 二、异常处理代码示例1、错误代码示例 - 抛出异常 / 不捕获异常2、正确代码示例 - 抛出异常 / 捕获异常3、正确代码示例 - 抛出异常 / 捕获异常不处理继续抛出异常 一、异常处理 1、异常概…

接口测试入门8问(含答案+文档)

Q1&#xff1a;什么是接口测试&#xff0c;基础知识什么的讲讲吧&#xff01; A&#xff1a;你好&#xff0c;接口可以分下面几种 1、系统与系统之间的调用&#xff0c;比如银行会提供接口供电子商务网站调用&#xff0c;或者说&#xff0c;支付宝会提供接口给淘宝调用 2、上…

Table和HashBasedTable的使用案例

------------------- 1.普通使用 package org.example.testhashbasedtable;import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table;import java.util.Map;public class TestHashBasedTable {public static void main(String[] args) {Ta…

【方法】PowerPoint如何删除“限制编辑”?

如果PPT文件设置成“只读模式”&#xff0c;就会被限制编辑&#xff0c;也就是无法对PPT进行编辑或更改&#xff0c;那要如何删除这个“限制”呢&#xff1f; 下面小编会按照“无密码的只读方式”、“有密码的只读方式”以及“忘记了密码的只读方式”这3种情况&#xff0c;来说…

enote笔记法之附录2——5w1h2k关联词(ver0.22)

enote笔记法之附录2——5w1h2k关联词&#xff08;ver0.22&#xff09; 最上面的是截屏的完整版&#xff0c;分割线下面的是纯文字版本&#xff1a; 作者姓名&#xff08;本人的真实姓名&#xff09;&#xff1a;胡佳吉 居住地&#xff1a;上海 作者网名&#xff1a;EverSt…

【从删库到跑路 | MySQL总结篇】表的增删查改(进阶下)

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【MySQL学习专栏】&#x1f388; 本专栏旨在分享学习MySQL的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 目录 一、联合…

【接口技术】实验3:可编程并行接口8255

实验3 可编程并行接口8255实验 一、实验目的 1&#xff1a;了解8255芯片结构及编程方法。 2&#xff1a;了解8255输入/输出实验方法。 3&#xff1a;掌握8255控制键盘及显示电路的基本功能及编程方法。 4&#xff1a;掌握一般键盘和显示电路的工作原理。 二、实验内容 1&…

WS2812灯条基于WLED开源项目无门槛使用简介

WS2812灯条基于WLED开源项目无门槛使用简介 &#x1f4cc;项目github地址&#xff1a;https://github.com/Aircoookie/WLED&#x1f4cd;WLED详情地址&#xff1a;https://kno.wled.ge/&#x1f388;网页在线烧录固件地址&#xff1a;https://install.wled.me/ ✨ 仅作为使用的…

安全技术与防火墙

目录 一、安全技术 1、安全技术 2、防火墙的分类 二、netfilter 1、netfilter简述 2、防火墙工具 1.iptables工具 2.netfilter的四表五链 3.内核中数据包的传输过程 4.三种报文流向 5.实操 总结&#xff1a;本章主要介绍了安全技术与防火墙 一、安全技术 1、安全技…

解决Unable to preventDefault inside passive event listener invocation.报错

报错信息&#xff1a; 这个报错大致说的是&#xff1a;无法在被动事件侦听器调用中防止Default 查了其他博主的解决办法&#xff1a;比如&#xff1a; 1、声明事件监听的时候设置为主动事件监听&#xff1a; window.addEventListener(‘touchmove’, handler, { passive: fal…