数据结构——红黑树(详解性质+C++模拟)

文章目录

  • 前言
  • 红黑树的概念
  • 红黑树的性质
  • 红黑树结点的定义
  • 红黑树的插入操作
    • 1. **按照二叉搜索树的规则插入新结点**
    • 2. 检测新节点插入后,红黑树的性质是否遭到破坏
  • 红黑树的验证
  • 总结

前言

本篇博客将为大家重点讲述红黑树这一数据结构,讲解其实现的方式即其具有的性质,并且最后用C++进行模拟实现这一数据结构,和AVL树相同,这篇文章也着重讲解关于其的插入操作。

由于其本质是一颗搜索二叉树,所以想要学习这一数据结构的同学需要首先了解二叉搜索树是什么,下面是博主以前写的关于二叉搜索树的博客链接,不清楚二叉搜索树是什么的伙伴可以先看看下面这篇博客:

博客链接: 数据结构 ——二叉搜索树(附C++模拟实现)

红黑树的概念

红黑树,是一种二叉搜索树,但在每个节点上增加了一个存储位表示节点的颜色,顾名思义可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。
在这里插入图片描述

红黑树的性质

现在,我们的问题就是,为什么红黑树能保证:其最长路径种节点个数不会超过最短的两倍呢?
这就要从红黑树的性质出发了,看如下性质:

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

由于满足这五个性质,我们考虑最短路径的情况: 路径上的结点都是黑色的,最长路径:路径上的结点都是红黑相间的,那么又由于每条路径都有相同数量的黑色结点,所以一定有最长路径不超过最短路径的两倍。


在了解了性质之后,接下来就我们就边模拟实现红黑树边讲解其是如何操作来维护这些特性的把!

红黑树结点的定义

//用一个枚举常量来表示颜色
enum Color
{RED,BLACK
};template<typename K, typename V>
struct RBTree_Node
{//为了提高对该结构操作的效率需要使用三叉链RBTree_Node<K, V>* _parent = nullptr;RBTree_Node<K, V>* _right = nullptr;RBTree_Node<K, V>* _left = nullptr;std::pair<K, V> _kv;int _color = RED;RBTree_Node(const std::pair<K,V>& kv):_kv(kv){}
};

这里有一个问题是,为什么要将结点的默认颜色给成红色

我们考虑插入操作,由于插入,我们有可能会破坏红黑树的性质,那么,大家觉得性质3和性质4哪个更好维护?
显然是性质3(不能有连续的红节点),而插入一个红节点一定不会破坏性质4,因此我们选择默认插入红节点。

红黑树的插入操作

与AVL树相同,红黑树也是在二叉搜索树的基础上加上其平衡条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索树的规则插入新结点

	bool insert(const std::pair<K, V>& kv){//如果root是空节点,直接插入即可if (!_root){node* newNode = new node(kv);//根节点的颜色一定是黑色newNode->_color = BLACK;_root = newNode;return true;}const K& newKey = kv.first;//寻找插入位置node* parent = nullptr, * cur = _root;while (cur){const K& Key = cur->_kv.first;parent = cur;if (Key < newKey)cur = cur->_right;else if (Key > newKey)cur = cur->_left;elsereturn false;}//到达此位置,说明插入的是一个新数据node* newNode = new node(kv);//插入一个红色节点,对旧树的影响最小//只需要修改连续的两个红色节点即可if (parent->_kv.first > newKey)parent->_left = newNode;elseparent->_right = newNode;newNode->_parent = parent;cur = newNode;//新结点插入后,需要检查红黑树的性质是否遭到破坏,如果破坏,需要进行一系列的调整//....

2. 检测新节点插入后,红黑树的性质是否遭到破坏

因为新节点默认颜色是红色,因此:如果双亲结点的颜色是黑色,那就不需要继续调整,但当双亲结点颜色是红色的时候,就违反了不能有连续红节点的性质,此时需要分类讨论来调整红黑树:

在此之前,先对一些符号做一些解释:
cur: 当前结点p:父节点
g:祖父节点 u:叔叔结点(父结点的兄弟结点)

首先由于插入前一定是红黑树,所以如果p为红,g一定为黑,因此有些情况不存在

情况一:cur为红,p为红,g为黑,u存在且为红
下图种的a,b, c, d, e是任意红黑树子树,也可以是空
在这里插入图片描述
此时,为了同时维护性质3和性质4,我们把p和u变黑,然后让g变红,这样这两条路径的黑色结点数量相当于没有变化。

在这里插入图片描述
但是调整为g之后,g有可能是整棵树的根节点,也有可能是子树的根,而如果不是根节点,我们还需要继续向上判断是否有连续红节点存在,如果是根节点,直接将其变为黑色之后即可结束调整

//修改红色节点的逻辑
while (parent && parent->_color == RED)
{//双亲是红色,那么爷爷节点一定是黑色if (parent == _root){parent->_color = BLACK;break;}node* grandfather = parent->_parent;//由于不知道p是g的左孩子还是右孩子,所以需要判断一下node* uncle = parent == grandfather->_left ? grandfather->_right : grandfather->_left;if (uncle && uncle->_color == RED){//通过这一个操作就可以保证两条路径上的黑色节点的数量不变grandfather->_color = RED;uncle->_color = parent->_color = BLACK;//由于将爷爷节点变成红色,所以有可能会导致前面的节点也出现连续红节点的情形,需要继续判断cur = grandfather;parent = cur->_parent;}else{//...}

对于接下来两种情况,需要使用AVL树中的旋转操作,如果有不知道旋转操作如何实现的可以看看博主的另一篇博客:
链接 --> AVL详解
在这篇文章中有详细介绍旋转如何实现的部分,大家可以通过目录直接跳转观看即可。

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

  1. u不存在
    在这里插入图片描述

如果u节点不存在,那么cur一定是新插入的结点,如果cur不是新插入的结点,那么根据性质3: cur和p一定有一个在调整前是黑色,那么以g为根结点的树就不满足红黑树的性质四:每条路径黑色结点个数相同

  1. u结点存在并且是黑色
    在这里插入图片描述

如果u结点存在并且是黑色,那么cur结点原来的颜色一定是黑色,现在是红色的原因是cur子树调整的过程中变成了红色。

这是由于如果cur是新增结点,那么在插入cur之前就已经不满足性质四,p,g路径上的黑色结点数目一定比u,g路径上的黑色结点数目少

对于这种情况,如果

p为g的左孩子,cur为p的左孩子,则进行右单旋操作
如果p为g的右孩子,cur为p的右孩子,则进行左单旋操作。
最后将p变成黑色,g变成红色即可完成调整
对于上图中的情况,旋转完之后得到:
在这里插入图片描述

我们来看一下为什么旋转之后能够维护红黑树的性质:
首先,很容易可以发现旋转只会对旋转前g的子树造成影响,并且通过图可以得知a, b, c, d, e这五个子树的路径上的黑色结点数量和旋转前并没有发生变化,因此这样子的操作是完全可行的。

并且如果我们不考虑各个结点上的值的话,这种做法可以理解为让p的左边路径少一个红节点,p的右边路径多一个红节点,并没有改变黑结点的数量,所以这个方法可行。

情况三:cur为红色,p为红,g为黑,u不存在/u存在且为黑
该情况和情况二不同的地方在于p和cur在其双亲的不同位置,对于这种情况,我们需要使用双旋的方法。
在这里插入图片描述

  1. 如果p为g的左孩子,cur为p的右孩子,则针对p做左单旋,之后就转换成了情况2,再使用一次右单旋即可恢复平衡
  2. 如果p为g的右孩子,cur为p的左孩子,则针对p做右单旋,同样转换为情况2,然后再使用一次左单旋即可。

上图第一步转换完成后:
在这里插入图片描述

由于这里的第一步是为了转换成情况2,所以我们只需要分析第一步是否会破坏红黑树的性质即可。

根据上图可知,第一步旋转完成之后,五颗子树的黑节点数量也斌没有发生变化,因此方法可行。

如此,我们就分析完了所有调整操作了,下面附上完整插入代码:


public:bool insert(const std::pair<K, V>& kv){//如果root是空节点,直接插入即可if (!_root){node* newNode = new node(kv);//根节点的颜色一定是黑色newNode->_color = BLACK;_root = newNode;return true;}const K& newKey = kv.first;//寻找插入位置node* parent = nullptr, * cur = _root;while (cur){const K& Key = cur->_kv.first;parent = cur;if (Key < newKey)cur = cur->_right;else if (Key > newKey)cur = cur->_left;elsereturn false;}//到达此位置,说明插入的是一个新数据node* newNode = new node(kv);//插入一个红色节点,对旧树的影响最小//只需要修改连续的两个红色节点即可if (parent->_kv.first > newKey)parent->_left = newNode;elseparent->_right = newNode;newNode->_parent = parent;cur = newNode;//修改红色节点的逻辑while (parent && parent->_color == RED){//双亲是红色,那么爷爷节点一定是黑色if (parent == _root){parent->_color = BLACK;break;}node* grandfather = parent->_parent;//由于不知道p是g的左孩子还是右孩子,所以需要判断一下node* uncle = parent == grandfather->_left ? grandfather->_right : grandfather->_left;if (uncle && uncle->_color == RED){//通过这一个操作就可以保证两条路径上的黑色节点的数量不变grandfather->_color = RED;uncle->_color = parent->_color = BLACK;//由于将爷爷节点变成红色,所以有可能会导致前面的节点也出现连续红节点的情形,需要继续判断cur = grandfather;parent = cur->_parent;}else{//如果叔叔不存在或者叔叔节点位黑色,需要进行旋转操作bool parent_in_right = true, cur_in_right = true;if (grandfather->_left == parent) parent_in_right = false;if (parent->_left == cur) cur_in_right = false;if (parent_in_right && cur_in_right){rotateL(grandfather);grandfather->_color = RED;parent->_color = BLACK;}else if (!parent_in_right && !cur_in_right){rotateR(grandfather);grandfather->_color = RED;parent->_color = BLACK;}else if (parent_in_right && !cur_in_right){rotateRL(grandfather);cur->_color = BLACK;grandfather->_color = RED;}else{rotateLR(grandfather);cur->_color = BLACK;grandfather->_color = RED;}_root->_color = BLACK;break;}}//如果插入节点的双亲是一个黑节点,不需要处理return true;}
private:void rotateL(node* parent){node* cur = parent->_right, * curL = cur->_left;node* ppnode = parent->_parent;//连接cur和parentcur->_left = parent;parent->_parent = cur;//连接parent和curLparent->_right = curL;if (curL) curL->_parent = parent;//连接ppnode和curcur->_parent = ppnode;if (!ppnode) _root = cur;else{if (ppnode->_left == parent)ppnode->_left = cur;else if (ppnode->_right == parent)ppnode->_right = cur;elsethrow "二叉树连接异常";}}void rotateR(node* parent){node* cur = parent->_left, * ppnode = parent->_parent;node* curR = cur->_right;//连接cur和parentcur->_right = parent;parent->_parent = cur;//连接parent和curRparent->_left = curR;//注意判断curR为空的情况if (curR) curR->_parent = parent;//连接ppnode和curcur->_parent = ppnode;if (!ppnode)_root = cur;else{if (ppnode->_right == parent)ppnode->_right = cur;else if (ppnode->_left == parent)ppnode->_left = cur;elsethrow "二叉树链接异常\n";}}void rotateRL(node* parent){rotateR(parent->_right);rotateL(parent);}void rotateLR(node* parent){rotateL(parent->_left);rotateR(parent);}

红黑树的验证

完成了红黑树的插入操作,我们当然要有办法验证手搓的红黑树是否满足其性质。
红黑树的检测分为两部分:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质
public:bool isRBTree() { return _isRBTree(_root); }
private:bool _isRBTree(node* root){if (!root) return true;if (root->_color == RED) return false;bool RB1 = no_seriesRed(root);if (!RB1)return false;int blackNum = -1;bool RB2 = equalBlack(root, 0, blackNum);if (!RB2)return false;return true;}//验证没有连续的红节点bool no_seriesRed(node* root){if (!root) return true;if (root->_color == RED)if (root->_parent && root->_parent->_color == RED){std::cout << "连续红节点\n";return false;}return no_seriesRed(root->_left)&& no_seriesRed(root->_right);}//验证每条路径上的黑色结点数量是否都相同bool equalBlack(node* root, int now, int& blackNum){if (!root){if (blackNum == -1) blackNum = now;else if (now != blackNum){std::cout << "黑节点数量不一致\n";return false;}return true;}if (root->_color == BLACK) now += 1;return equalBlack(root->_left, now, blackNum)&& equalBlack(root->_right, now, blackNum);}

总结

以上就是关于红黑树的所有内容啦,大家如果想要真正掌握红黑树,光看代码肯定是不够的,一定要自己下去模拟实践一次才能真正的掌握其核心,并且一定要深入理解它的性质!!以上就是本篇博客的所有内容啦,如果博主有哪里写的有问题或者有大家疑惑的地方,欢迎在评论区指出!!

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

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

相关文章

One Thread One Loop主从Reactor模型⾼并发服务器

One Thread One Loop主从Reactor模型⾼并发服务器 文章目录 One Thread One Loop主从Reactor模型⾼并发服务器一些补充HTTP服务器Reactor 模型eventfd通用类Any 目标功能模块划分&#xff1a;SERVER模块Buffer模块&#xff1a;编写思路&#xff1a;接口设计&#xff1a;具体实现…

毕设-原创医疗预约挂号平台分享

医疗预约挂号平台 不是尚医通项目&#xff0c;先看项目质量&#xff08;有源码论文&#xff09; 项目链接&#xff1a;医疗预约挂号平台git地址 演示视频&#xff1a;医疗预约挂号平台 功能结构图 登录注册模块&#xff1a;该模块具体分为登录和注册两个功能&#xff0c;这些…

Linux:环境变量、地址空间

目录 一、环境变量 1、什么是环境变量 2、常见的环境变量 3、环境变量相关命令 二、地址空间 1、进程地址空间 2、虚拟地址空间 一、环境变量 1、什么是环境变量 首先先举个环境变量的例子&#xff1a; 我们在Linux中&#xff0c;运行ls、pwd之类的命令&#xff0c;直…

力扣 -- 873. 最长的斐波那契子序列的长度

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int lenLongestFibSubseq(vector<int>& nums) {int nnums.size();unordered_map<int,int> hash;for(int i0;i<n;i){hash[nums[i]]i;}int ret2;vector<vector<int>> dp(n,v…

Python之元组

Python之元组 元组tuple 一个有序的元素组成的集合使用小括号 ( ) 表示元组是不可变对象 tuple(), (), type(()) # 空元组 ((), (), tuple)(1,), (1) # 元组中只有1必须加逗号&#xff0c;否则就是1了 # ((1,), 1)x 1, 2 # 以逗号分隔的内容会形成元组&#xff0c;封装元组x…

DBC配置SecOC属性

关联文章:Autosar基础——车载信息安全SecOC 属性定义的规范详细介绍请参考 ①dbc属性定义 ②Vector DBC属性定义规则 文章目录 一、SecOC简介二、DBC文件中的SecOC属性三、配置SecOC属性设置SecOC的属性设置同步报文的属性设置同步请求报文的属性一、SecOC简介 在车载网络中…

数据分析篇-数据认知分析

一简介 数据认知分析&#xff0c;实际是对数据的整体结构和分布特征进行分析&#xff0c;是对整个数据外在的认识&#xff0c;也是数据分析的第一步。对于数据认知的分析&#xff0c;一般会考虑分散性、位置特性、变量的相关性等&#xff0c;一般会考虑平均数、方差、极差、峰…

朋友圈怎么定点发朋友圈?

微信朋友圈是我们日常生活中常用的社交媒体之一。但有时我们忙碌而可能会忘记发布朋友圈&#xff0c;或是因时间不合适而无法发布。那么&#xff0c;有没有一种方法可以在规定的时间内自动发布朋友圈呢&#xff1f; 当然有啦&#xff01; 定时发朋友圈可以帮助我们在特定时间点…

7.Tensors For Beginneers - Convector Components

介绍协向量时&#xff0c;曾说过它们有点像 行向量&#xff0c; 行向量确实以某种方式代表了协向量&#xff0c; 这里说明一下&#xff1a; 协向量是不变的&#xff1b; 协向量组件是可变的。 协向量不依赖坐标系&#xff0c;协向量的组件取决于坐标系。 当我们说协向量具有组…

Javascript文件上传

什么是文件上传 文件上传包含两部分&#xff0c; 一部分是选择文件&#xff0c;包含所有相关的界面交互。一部分是网络传输&#xff0c;通过一个网络请求&#xff0c;将文件的数据携带过去&#xff0c;传递到服务器中&#xff0c;剩下的&#xff0c;在服务器中如何存储&#xf…

【11】c++设计模式——>单例模式

单例模式是什么 在一个项目中&#xff0c;全局范围内&#xff0c;某个类的实例有且仅有一个&#xff08;只能new一次&#xff09;&#xff0c;通过这个唯一的实例向其他模块提供数据的全局访问&#xff0c;这种模式就叫单例模式。单例模式的典型应用就是任务队列。 为什么要使…

C++(STL容器适配器)

前言&#xff1a; 适配器也称配接器&#xff08;adapters&#xff09;在STL组件的灵活组合运用功能上&#xff0c;扮演着轴承、转换器的角色。 《Design Patterns》对adapter的定义如下&#xff1a;将一个class的接口转换为另一个class的接口&#xff0c;使原本因接口不兼容而…

Python之字符串构造

Python之字符串构造 字符串str 一个个字符组成的有序的序列&#xff0c;是字符的集合使用单引号、双引号、三引号引住的字符序列字符串是不可变对象&#xff0c;是字面常量 Python3起&#xff0c;字符串都是Unicode类型 x abcde使用for循环遍历x的值&#xff0c;打印并查看…

2023 年 Web 安全最详细学习路线指南,从入门到入职(含书籍、工具包)【建议收藏】

第一个方向&#xff1a;安全研发 你可以把网络安全理解成电商行业、教育行业等其他行业一样&#xff0c;每个行业都有自己的软件研发&#xff0c;网络安全作为一个行业也不例外&#xff0c;不同的是这个行业的研发就是开发与网络安全业务相关的软件。 既然如此&#xff0c;那其…

从零开始学习线性回归:理论、实践与PyTorch实现

文章目录 &#x1f966;介绍&#x1f966;基本知识&#x1f966;代码实现&#x1f966;完整代码&#x1f966;总结 &#x1f966;介绍 线性回归是统计学和机器学习中最简单而强大的算法之一&#xff0c;用于建模和预测连续性数值输出与输入特征之间的关系。本博客将深入探讨线性…

解决报错: require is not defined in ES module scope

用node启动mjs文件报错&#xff1a;require is not defined in ES module scope 现象如下&#xff1a; 原因&#xff1a; 文件后缀是mjs, 被识别为es模块&#xff0c;但是node默认是commonjs格式&#xff0c;不支持也不能识别es模块。 解决办法&#xff1a;把文件后缀从.mjs改…

Spring web security

儅使用spring的web security時&#xff0c;默認會轉向自帶的spring security example page。而不會轉向error page。 TODO: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> &l…

【智能家居项目】裸机版本——字体子系统 | 显示子系统

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《智能家居项目》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 今天实现上图整个项目系统中的字体子系统和显示子系统。 目录 &#x1f004;设计思路&#x1…

【网络安全-信息收集】网络安全之信息收集和信息收集工具讲解

一&#xff0c;域名信息收集 1-1 域名信息查询 可以用一些在线网站进行收集&#xff0c;比如站长之家 域名Whois查询 - 站长之家站长之家-站长工具提供whois查询工具&#xff0c;汉化版的域名whois查询工具。https://whois.chinaz.com/ 可以查看一下有没有有用的信息&#xf…

【熬夜爆肝版】JAVA基础入门专栏——1.JAVA开发入门

JAVA开发入门 1、Java概述1&#xff09;起源2&#xff09;特点3&#xff09;应用领域 2、JDK1&#xff09;定义2&#xff09;作用3&#xff09;组成4&#xff09;JDK版本与兼容性5&#xff09;JDK的安装与配置6&#xff09;JDK的发行版 3、系统环境变量1&#xff09;定义2&…