【C++】红黑树的原理与实现

 

文章目录

一、引言

二、红黑树的概念与性质

2、1 红黑树的概念

2、2 红黑树的性质

三、红黑树的定义与实现

3、1 红黑树的定义

3、2 插入新节点

3、2、1 默认插入红色节点

3、3 插入情况分类

3、3、1 情况一(根据颜色向上调整)

3、3、2 情况二(单次旋转+变色)

3、3、3 情况三(两次旋转+变色)

3、4 插入的完整代码实现

四、红黑树的检验

五、性能分析

六、总结


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:C++、数据结构 👀

💥 标题:红黑树💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️ 

一、引言

  红黑树是一种自平衡的二叉搜索树,它在计算机科学中扮演着重要的角色。由于其高效的插入、删除和查找操作,红黑树被广泛应用于各种数据结构和算法的实现中。红黑树的设计旨在保持树的平衡,从而确保各种操作的时间复杂度能够保持在较低的范围内。 

 当我们学完 AVL树 后,我们再学红黑树相对来说就会简单一点。红黑树可以看成是AVL树的升级版。

  本篇文章会对红黑树的实现原理进行详解。同时还会给出红黑树的C++实现代码。希望本篇文章会对你有所帮助。

二、红黑树的概念与性质

2、1 红黑树的概念

  红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。

2、2 红黑树的性质

 红黑树也是一颗很特殊的树,它具有如下的性质

  1. 每个结点不是红色就是黑色。
  2. 根节点是黑色的 。
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的(不能有两个连续的红色节点)。
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 (从根节点到空节点为一条路径,则空节点的数量为该红黑树的路径数量,且每条路径的黑色节点数量相同)。
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
  这里就有一个疑问了:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?
  根据上述的性质三我们知道,一个路径最短的情况下就是所有节点的颜色为黑色。同时最长的情况是一红一黑交叉出现。但是我们不要忘记了性质四每条路径的黑色节点数量相同,那么最长的路径就是最短的路径两倍了,并没有超过两倍。

  通过以上的红黑树特性,我们可以保证红黑树在进行插入、删除等操作时,始终能够保持树的平衡性,从而保证了各种操作的时间复杂度始终在对数级别内。在接下来的章节中,我们将深入探讨红黑树的插入操作的实现细节,以及时间复杂度的分析,进一步理解红黑树的原理和实现。 

三、红黑树的定义与实现

3、1 红黑树的定义

  红黑树也是一种二叉搜索树,它的每个节点包含一个关键字(键值对)、左右孩子指针和父结点指针。每个节点还包含一个记录颜色的变量该变量用来表示该节点是红色节点还是黑色节点。 实现代码如下:

enum Colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;pair<K, V> _kv;Colour _col;RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv){}
};

  表示颜色我们采用枚举的方式,以便整篇的代码比较整洁且易于理解。

3、2 插入新节点

  红黑树的插入操作是通过对二叉搜索树的基本插入操作结合颜色调整和旋转操作来实现的。插入算法的概述如下:

  1. 首先,将插入的节点按照普通的二叉搜索树插入操作,将其放置在合适的位置上。
  2. 将插入节点标记为红色,这样不会违反红黑树的特性。
  3. 检查插入节点的父节点及其祖父节点、叔叔节点等情况,根据不同的情况进行颜色调整和旋转操作,以保持红黑树的平衡性和特性。
  4. 最后,确保根节点是黑色的,以满足红黑树的根节点特性。

  插入操作的核心是对树进行颜色调整和旋转,以确保插入后仍然满足红黑树的特性。具体的左旋和右旋操作将在后续小节中详细解释,而插入修正过程则会对颜色调整的各种情况进行逐步解析。通过这些操作,红黑树能够在插入新节点时保持平衡,从而保证了各种操作的高效性能。

  接下来,我们将逐步深入探讨插入操作的具体细节,以便更好地理解红黑树的实现原理。

3、2、1 默认插入红色节点

  在具体的学习插入操作之前,我们应该想一下新插入的节点是插入红色节点呢还是插入黑色节点呢?我们不妨自己先结合着红黑树的性质思考一下。

  试想,我们新插入的节点是黑色节点,那么就违背了上述的性质四。那么影响的就是整棵树。大概率需要很复杂的旋转和调整才能恢复。假如新插入的节点是红色节点,新插入的节点的父节点可能为黑色,也可能为红色。当父节点为黑色时,并无影响。当父节点为红色时,违背了上述的性质三,影响的只是改父节点所在的子树。

  将新插入的节点设置为红色有两个主要原因:

  1. 保持黑色高度平衡:红黑树要求在任意路径上的黑色节点数量是相同的,这被称为黑色高度平衡。通过将新插入的节点设置为红色,可以确保其不会破坏现有的黑色高度平衡。

  2. 简化平衡调整:当插入新节点后,可能会导致红黑树的性质被破坏,需要进行平衡调整来恢复性质。将新节点设置为红色可以简化平衡调整的过程,因为红色节点的插入更容易适应红黑树的性质。如果将新节点设置为黑色,则可能需要对树进行更多的旋转和重新着色操作。

  总之,将新插入的节点设置为红色有助于保持红黑树的平衡性和减少平衡调整的复杂性。

3、3 插入情况分类

  我们先看如下情况,在值为1的黑色节点左侧插入一个新节点: 

  插入完之后,并不影响改红黑树的平衡,且满足红黑树的性质。

  我们再看另一种情况,在值为22的红色节点左侧插入一个新节点:

  我们发现,此时新插入的节点影响到了红黑树的平衡。那要是插入到值为22的红色节点右侧呢?或者插入到值为6的节点,又或者插入到值为27的节点,都需要我们进行调整。由于情况太多,所以对需要进行调整的情况进行了分类。

  接下来对需要调整的各种情况分类进行详解。

3、3、1 情况一(根据颜色向上调整)

  具体需要调整的状态如下图(抽象图):

  注:cur为当前所在位置的节点;p(parent)为cur的父节点;u(uncle)为cur的叔叔节点;g(grandfather)为cur的祖宗节点

  我们发现上述的情况cur为红,p为红,g为黑,u存在且为红违背了性质三。那怎么进行调整呢?调整的方法很简单:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。具体如下图:

  如果g是根节点,调整完之后,需要将g改变为黑色。如果g是子树,g一定有双节点,且如果g的双亲是红色,需要继续重复此过程向上调整。 具体如下图:

  上图中的cur可能是新增节点,也可能是更新后的节点,具体如下图:

  当然,新增的红色节点可以是任意孩子位置, 只要是满足cur为红,p为红,g为黑,u存在且为红,就划分到这一类。 

3、3、2 情况二(单次旋转+变色)

  我们知道,只有p(父节点)是红色时,才需要进行调整。但是u(叔叔节点)不一定为红色,也有可能为黑色,甚至u(叔叔节点)可能就不存在。具体如下图:

  u的情况有两种,不同情况对应的情况说明:

  1. 如果u节点不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个数相同.
  2. 如果u节点存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的,现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色。

  虽然u的情况有两种,但是处理方法是一样的。这时,只通过颜色调整是不行了。具体的调整方法是: p为g的左孩子,cur为p的左孩子,则进行对g进行右单旋转;相反, p为g的右孩子,cur为p的右孩子,则对g进行进行左单旋转。p、g变色--p变黑,g变红。具体如下图:

3、3、3 情况三(两次旋转+变色)

  当u(叔叔节点)为黑色或者不存在时,cur的位置也是影响旋转的关键。我们看如下情况:

  我们发现直接对g(祖宗节点)进行旋转并不能很好的解决问题。那能不能先对p(父亲节点)进行旋转呢?具体调整方法:p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,p为g的右孩子,cur为p的左孩子,则针对p做右单旋转。则转换成了情况2。对p旋转完后,具体情况如下图:

  那我们再进行针对情况二的旋转和变色不就完成了吗!!! 

3、4 插入的完整代码实现

  当我们理解了上述的插入和调整思路后,我们来看一下代码的实现:

	bool Insert(const pair<K, V>& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(kv);cur->_col = RED;if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfater = parent->_parent;assert(grandfater);assert(grandfater->_col == BLACK);// 关键看叔叔if (parent == grandfater->_left){Node* uncle = grandfater->_right;// 情况一 : uncle存在且为红,变色+继续往上处理if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}// 情况二+三:uncle不存在 + 存在且为黑else{// 情况二:右单旋+变色//     g //   p   u// cif (cur == parent->_left){RotateR(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:左右单旋+变色//     g //   p   u//     cRotateL(parent);RotateR(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}else // (parent == grandfater->_right){Node* uncle = grandfater->_left;// 情况一if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}else{// 情况二:左单旋+变色//     g //   u   p//         cif (cur == parent->_right){RotateL(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:右左单旋+变色//     g //   u   p//     cRotateR(parent);RotateL(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}}_root->_col = BLACK;return true;}

四、红黑树的检验

  当我们写完插入时,怎么判断该树是否符合红黑树呢?只是中序打印肯定是不够的,因为中序打印只能说明该树是二叉搜索树。那怎么办呢?

  判断最长路径是否不大于最短路径长度的二倍行不行呢?好像也并不行。为什么呢?那要是还有连续的红节点呢?又或者每条路径的黑色节点的数量不同呢?

  我们发现,当满足性质三和性质四时,就会满足最长路径是否不大于最短路径长度的二倍。同时再判断一下根节点的颜色就行,具体代码如下:

public:void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){if (_root == nullptr){return true;}if (_root->_col == RED){cout << "根节点不是黑色" << endl;return false;}// 黑色节点数量基准值int benchmark = 0;/*Node* cur = _root;while (cur){if (cur->_col == BLACK)++benchmark;cur = cur->_left;}*/return PrevCheck(_root, 0, benchmark);}private:bool PrevCheck(Node* root, int blackNum, int& benchmark){if (root == nullptr){//cout << blackNum << endl;//return;if (benchmark == 0){benchmark = blackNum;return true;}if (blackNum != benchmark){cout << "某条黑色节点的数量不相等" << endl;return false;}else{return true;}}if (root->_col == BLACK){++blackNum;}if (root->_col == RED && root->_parent->_col == RED){cout << "存在连续的红色节点" << endl;return false;}return PrevCheck(root->_left, blackNum, benchmark)&& PrevCheck(root->_right, blackNum, benchmark);}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}

五、性能分析

  红黑树的插入操作的平均时间复杂度是O(log n),其中n是树中节点的数量。这是因为红黑树的插入操作涉及到一系列的旋转和修正操作,但这些操作都是局部的,不会导致整个树的高度增加太多。

  需要注意的是,虽然单次插入操作的时间复杂度是O(log n),但在一系列插入操作后,为了保持红黑树的平衡性,可能需要进行多次的旋转和修正操作。但由于这些操作的时间复杂度都是常数级别的,不会随着插入操作的增加而显著增加,所以整体插入操作的平均时间复杂度仍然是O(log n)。

  红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是 O(log n) ,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优
AVL树是另一种自平衡二叉搜索树,与红黑树相比,AVL树要求更为严格的平衡性,每个节点的左子树和右子树的高度差(平衡因子)不超过1。这使得AVL树在查找操作上更快,但插入和删除操作可能需要更多的旋转

六、总结

  红黑树作为一种自平衡二叉搜索树,具有良好的平衡性质和高效的插入、删除、查找等操作,被广泛应用于各种数据结构和算法领域。通过合理的旋转操作和修正过程,红黑树能够保持树的平衡,从而保证了操作的时间复杂度在可接受的范围内。

  在本文中,我们深入探讨了红黑树的原理和实现细节。从基本概念开始,我们介绍了红黑树的定义、特性,然后详细讲解了插入操作的算法和旋转操作。接着,我们讨论了删除操作的实现和修正过程。通过分析插入和删除操作的时间复杂度,我们得出了红黑树在各种操作下的高效性能。

  总的来说,红黑树作为一种经典的数据结构,在算法和计算机科学领域中具有重要地位。通过深入学习和理解红黑树的原理和实现,读者不仅可以拓展自己的数据结构知识,还能够为解决实际问题提供有力的工具和思路。随着计算机科学的不断发展,红黑树及其相关的优化和扩展将继续在各个领域发挥重要作用。

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

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

相关文章

AIGC技术到底是什么?为什么这么火热?

AIGC技术到底是什么&#xff1f;为什么这么火热&#xff1f; ALCG技术到底是什么&#xff1f;AIGC技术的发展史AIGC技术特点AIGC技术主要用途ALGC技术未来发展 ALCG技术到底是什么&#xff1f; AIGC&#xff08;Artificial Intelligence in Game Creation&#xff09;技术是指…

OpenLayers实战,OpenLayers实现气象台风飓风运动轨迹运动动画,可调台风旋转速度和运动速度,静态图片旋转动画

专栏目录: OpenLayers实战进阶专栏目录 前言 本章使用OpenLayers实现气象中常用的台风或者飓风运动轨迹动画,支持调整台风图标旋转速度和运动速度。 不同的台风可以设置不同的运动速度和旋转速度,也可以通过变量控制图片不旋转。 本章图片使用静态png图片,并非gif动态图。…

中间件多版本冲突的4种解决方案和我们的选择

背景 在小小的公司里面&#xff0c;挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用&#xff0c;需要在里面支持多个版本的中间件客户端&#xff1b;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT&#xff0c;百度&#xff0c;googl…

在Python中定义Main函数

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 许多编程语言都有一个特殊的函数&#xff0c;当操作系统开始运行程序时会自动执行该函数。 这个函数通常被命名为main()&#xff0c;并且依据语言标准具有特定的返回类型和参数。 另一方面&#xff0c;Python解释器从文件…

SQL-每日一题【1179. 重新格式化部门表】

题目 部门表 Department&#xff1a; 编写一个 SQL 查询来重新格式化表&#xff0c;使得新的表中有一个部门 id 列和一些对应 每个月 的收入&#xff08;revenue&#xff09;列。 查询结果格式如下面的示例所示&#xff1a; 解题思路 1.题目要求我们重新格式化表&#xff0c;…

Sentieon | 应用教程: 关于读段组的建议

介绍 本文档描述了使用Sentieon Genomics软件时&#xff0c;推荐使用RGID字段以最小化潜在问题的用法。 本文档能帮助您确定设置所使用的bam文件中RG标签的不同字段的最佳实践方法。 RG字段及其用法的详细描述 RG字段的详细描述 SAM格式规范http://samtools.github.io/hts-…

Android Studio实现刮刮卡效果

代码和刮刮乐图片参考网络 实现效果 MainActivity import android.app.Activity; import android.os.Bundle;public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentV…

教雅川学缠论06-中枢

本系列文章之前讲的内容都只有上升和下降两类趋势&#xff0c;并没有提及盘整&#xff0c;在缠论中&#xff0c;中枢这个新词汇用来定义盘整&#xff0c;中枢&#xff1a; 1.至少由5条线段&#xff08;或笔&#xff09;组成 2.中枢是有方向的&#xff0c;中枢左右两侧外面的线&…

【locust】使用locust + boomer实现对接口的压测

目录 背景 环境安装 脚本编写 master slave节点&#xff08;golang/boomer&#xff09; 问题 资料获取方法 背景 很早之前&#xff0c;考虑单机执行能力&#xff0c;使用locust做过公司短信网关的压测工作&#xff0c;后来发现了一个golang版本的locust&#xff0c;性能…

替换开源LDAP,某科技企业用宁盾目录统一身份,为业务敏捷提供支撑

客户介绍 某高科技企业成立于2015年&#xff0c;是一家深耕于大物流领域的人工智能公司&#xff0c;迄今为止已为全球16个国家和地区&#xff0c;120余家客户打造智能化升级体验&#xff0c;场景覆盖海陆空铁、工厂等货运物流领域。 该公司使用开源LDAP面临的挑战 挑战1 开源…

01《Detecting Software Attacks on Embedded IoT Devices》随笔

2023.08.05 今天读的是一篇博士论文 论文传送门&#xff1a;Detecting Software Attacks on Embedded IoT Devices 看了很长时间&#xff0c;发现有一百多页&#xff0c;没看完&#xff0c;没看到怎么实现的。 摘要 联网设备的增加使得嵌入式设备成为各种网络攻击的诱人目标&…

c#设计模式-创建型模式 之 工厂模式

前言&#xff1a; 工厂模式&#xff08;Factory Pattern&#xff09;是一种常用的对象创建型设计模式。该模式的主要思想是提供一个创建对象的接口&#xff08;也可以是抽象类、静态方法等&#xff09;&#xff0c;将实际创建对象的工作推迟到子类中进行。这样一来&#xff0c…

第一百二十四天学习记录:C++提高:STL-deque容器(上)(黑马教学视频)

deque容器 deque容器基本概念 功能&#xff1a; 双端数组&#xff0c;可以对头端进行插入删除操作 deque与vector区别 vector对于头部的插入删除效率低&#xff0c;数据量越大&#xff0c;效率越低 deque相对而言&#xff0c;对头部的插入删除速度比vector快 vector访问元素的…

cpu util margin,cpu freq margin

【cpufreq governor】cpu util 和 cpu margin怎么计算的_悟空明镜的博客-CSDN博客 cpu util margin&#xff0c;cpu freq margin 根据policy_util schedtune_margin 作为算力选对应的cpu cluster或调频

dubbo之整合SpringBoot

目录 zookeeper安装 1.拉取ZooKeeper镜像 2.新建文件夹 3.挂载本地文件夹并启动服务 4.查看容器 5.进入容器&#xff08;zookeeper&#xff09; Dubbo Admin安装 1.下载dubbo-admin 2.zip包解压 3.修改配置文件 4.打包项目 5.启动jar 6.访问 构建项目 api模块 1.创建…

24届近5年上海理工大学自动化考研院校分析

今天学姐给大家带来的是上海理工大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、上海理工大学 学校简介 上海理工大学&#xff08;University of Shanghai for Science and Technology&#xff09;是一所以工学为主&#xff0c;工学、理学、经济学、管理学、文…

php实现登录的例子

界面&#xff1a; 登录界面login.html代码&#xff1a; <!DOCUMENT html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"…

【word密码】word设置只读,如何取消?

Word文件打开之后发现是只读模式&#xff0c;那么我们如何取消word文档的只读模式呢&#xff1f;今天给大家介绍几种只读模式的取消方法。 属性只读 有些文件可能是在文件属性中添加了只读属性&#xff0c;这种情况&#xff0c;我们只需要点击文件&#xff0c;再次查看文件属…

sd-roop换脸插件安装

安装步骤 首先看官方教程 sd-webui-roop插件&#xff0c; 如下&#xff1a; 执行 pip install insightface0.7.3在web-ui 界面&#xff0c;插件菜单&#xff0c;从网址安装 https://github.com/s0md3v/sd-webui-roopweb-ui 界面重启如果遇到 NoneType object has no attribu…

状态模式——对象状态及其转换

1、简介 1.1、概述 在软件系统中&#xff0c;有些对象也像水一样具有多种状态&#xff0c;这些状态在某些情况下能够相互转换&#xff0c;而且对象在不同的状态下也将具有不同的行为。为了更好地对这些具有多种状态的对象进行设计&#xff0c;可以使用一种被称为状态模式的设…