【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.hqwc.cn/news/231978.html

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

相关文章

数据结构---堆

1.堆的概念及结构 堆的性质&#xff1a; 堆中某个节点的值总是不大于或不小于其父节点的值堆总是一棵完全二叉树 2.举例说明 堆一般是把数组数据看做是一棵完全二叉树 小堆要求&#xff1a;任意一个父亲<孩子大堆要求&#xff1a;任意一个父亲>孩子 比如&#xff1…

CV计算机视觉每日开源代码Paper with code速览-2023.11.23

点击CV计算机视觉&#xff0c;关注更多CV干货 论文已打包&#xff0c;点击进入—>下载界面 点击加入—>CV计算机视觉交流群 1.【基础网络架构&#xff1a;Transformer】White-Box Transformers via Sparse Rate Reduction: Compression Is All There Is? 论文地址&am…

云时空社会化商业 ERP 系统 gpy 文件上传漏洞复现

0x01 产品简介 时空云社会化商业ERP&#xff08;简称时空云ERP&#xff09; &#xff0c;该产品采用JAVA语言和Oracle数据库&#xff0c; 融合用友软件的先进管理理念&#xff0c;汇集各医药企业特色管理需求&#xff0c;通过规范各个流通环节从而提高企业竞争力、降低人员成本…

智能井盖传感器怎么监测井盖出现倾斜?

智能井盖传感器是一种先进的智能设备&#xff0c;能够二十四小时连续监测井盖是否出现倾斜。其工作原理主要是依靠内置的传感器&#xff0c;以及搭载的MEMS“芯”技术。便于智能井盖传感器实时感知到井盖的姿态变化&#xff0c;一旦发现有倾斜的现象&#xff0c;就会立即向运维…

Elasticsearch 快照如何工作?

作者&#xff1a;Lutf ur Rehman Elastic 提供许多由讲师指导的面对面和虚拟现场培训以及点播培训。 我们的旗舰课程是 Elasticsearch 工程师、Kibana 数据分析和 Elastic 可观测性工程师。 所有这些课程都会获得认证。有关这些课程的详细介绍&#xff0c;请参考我之前的文章 “…

从0开始学习JavaScript--JavaScript 模板字符串的全面应用

JavaScript 模板字符串是 ES6 引入的一项强大特性&#xff0c;它提供了一种更优雅、更灵活的字符串拼接方式。在本文中&#xff0c;将深入探讨模板字符串的基本语法、高级用法以及在实际项目中的广泛应用&#xff0c;通过丰富的示例代码带你领略模板字符串的魅力。 模板字符串…

ArcGIS制作某村土地利用现状图

1. 根据坐落单位名称属性选择并提取作图数据 (1) 将“作图线状地物”、“作图图班”和“村庄”图层加入ARCGIS&#xff08;右键Layers-Add data&#xff09;&#xff0c;选择相应路径下的文件加载即可。 (2) 按属性来提取作图村庄的地类图班、线状地物和村界文件&#xff08;…

element-plus 使用密码输入框的自定义图标

<el-inputv-model"ruleFormPassword.newPassword"placeholder"请输入新密码":type"showPassword ? text : password":style"{ width: 360px }"><template #suffix><span class"input_icon" click"swit…

智能优化算法应用:基于帝国主义竞争算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于帝国主义竞争算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于帝国主义竞争算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.帝国主义竞争算法4.实验参数设定5.算…

昨日一题 1670. 设计前中后队列(中等,列表)

维护左右两个队列&#xff0c;控制左队列的长度比右队列长&#xff0c;且不超过1pushFront 往左队列的左边添加元素pushMiddle 往左队列的右边或者右队列的左边添加元素其余同理&#xff0c;可以参照代码 class FrontMiddleBackQueue:def __init__(self):self.llist, self.rli…

【Linux】Linux中git的基本使用(三板斧)

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和Linux还有算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 …

CentOS 7 部署 Nacos (单机版)

CentOS 7 部署 Nacos &#xff08;单机版&#xff09; 1. 下载 Nacos 安装包 历史版本&#xff1a;https://github.com/alibaba/nacos/releases/ 我选的是 2.1.0 版本&#xff0c;https://github.com/alibaba/nacos/releases/download/2.1.0/nacos-server-2.1.0.tar.gz 2. …