

🛸个人主页: dragoooon34
🚁所属专栏: C++
🚀操作环境: Visual Studio 2022

目录
[1-1 🍕红黑树的定义](#1-1 🍕红黑树的定义)
[1-2 🍔红黑树的性质](#1-2 🍔红黑树的性质)
[1-3 🍟红黑树的特点](#1-3 🍟红黑树的特点)
[2-1 🌭抽象图](#2-1 🌭抽象图)
[2-2 🍿插入流程](#2-2 🍿插入流程)
[2-3 🧂单纯染色](#2-3 🧂单纯染色)
[2-4 🥓左单旋 + 染色](#2-4 🥓左单旋 + 染色)
[2-5 🥚右左双旋 + 染色](#2-5 🥚右左双旋 + 染色)
[2-6 🍳具体实现代码](#2-6 🍳具体实现代码)
[2-6 🧇注意事项及调试技巧](#2-6 🧇注意事项及调试技巧)
[三、😂AVL树 VS 红黑树](#三、😂AVL树 VS 红黑树)
[3-1 🥞红黑树的检验](#3-1 🥞红黑树的检验)
[3-2 🧈性能对比](#3-2 🧈性能对比)
🌞前言
红黑树是平衡二叉搜索树中的一种,红黑树性能优异,广泛用于实践中,比如 Linux 内核中的 CFS 调度器就用到了红黑树,由此可见红黑树的重要性。红黑树在实现时仅仅依靠 红 与 黑 两种颜色控制高度,当触发特定条件时,才会采取 旋转 的方式降低树的高度,使其平衡
🌜正文
一、😀认识红黑树
红黑树 由 德国·慕尼黑大学 的 Rudolf Bayer 教授于
1978年发明,后来被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的 红黑树
红黑树 在原 二叉搜索树 节点的基础上,加上了 颜色 Color 这个新成员,并通过一些规则,降低二叉树的高度如果说 AVL 树是天才设计,那么 红黑树 就是 天才中的天才设计,不同于 AVL 树的极度自律,红黑树 只在条件符合时,才会进行 旋转降高度,因为旋转也是需要耗费时间的
红黑树在减少旋转次数时,在整体性能上仍然没有落后 AVL 树太多
先来一睹 红黑树 的样貌
注:红黑树在极限场景下,与 AVL 树的性能差不超过 2 倍
1-1 🍕红黑树的定义
红黑树 也是 三叉链 结构,不过它没有 平衡因子 ,取而代之的是 颜色
红黑树 的节点定义如下:(这里是通过 枚举 定义的颜色)
cpp//枚举出 红、黑 两种颜色 enum Color { RED, BLACK }; //红黑树的节点类 template<class K, class V> struct RBTreeNode { RBTreeNode(std::pair<K, V> kv) :_left(nullptr) , _right(nullptr) , _parent(nullptr) , _kv(kv) , _col(RED) //默认新节点为红色,有几率被调整 { } RBTreeNode<K, V>* _left; RBTreeNode<K, V>* _right; RBTreeNode<K, V>* _parent; std::pair<K, V> _kv; Color _col; };注意: 定义新节点时,颜色可以为 红 也可以为 黑,推荐为 红色,具体原因后面解释
1-2 🍔红黑树的性质
结构上的绝妙注定了其离不开规则的限制 ,红黑树 有以下几条性质:
- 每个节点不是 红色 就是 黑色
- 根节点是 黑色 的
- 如果一个节点是 红色 的,那么它的两个孩子节点都不能是 红色 的(不能出现连续的红节点)
- 对于每个节点,从该节点到其所有后代的 NIF 节点的简单路径上,都包含相同数目的黑色节点(每条路径上都有相同数目的 黑色 节点)
- 每个叶子节点的 nullptr 称为 NIF 节点,并且默认为黑色,此处黑色仅用于路径判断,不具备其他含义
在这些规则的限制之下,红黑树 就诞生了
红黑树 的性质还是比较重要的,可以花点时间结合图示深入理解
说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道⼀下这个概念即可。
1-3 🍟红黑树的特点
红黑树 在插入节点后,可以选择新节点为 红 或 黑
- 如果为 红 ,可能违反原则3,需要进行调整
- 如果为 黑,必然违反原则4,并且因为这一条路,影响了其他所有路径,调整起来比较麻烦
因此 推荐在插入时,新节点默认为 红色 ,插入后,不一定调整,即使调整,也不至于 影响全局
显然,红黑树 中每条路径都是 红黑相间 的,因为不能出现连续的 红节点 ,所以 黑节点的数量 >= 红节点
也就是说:红黑树中,最长路径至多为最短路径的两倍
- 最长路径:红黑相间
- 最短路径:全是黑节点
上图中的 最短路径 为 3,最长路径 为 4,当然,最短路径 可以为 2
对于 AVL 树来说,下面这个场景必然 旋转降高度 ,但 红黑树 就不必,因为 没有违背性质
综上, 红黑树 是一种折中且优雅的解决方案,不像 AVL 树 那样极端(动不动就要旋转),而是只有触发特定条件时,才会发生旋转,并且在极端场景下, 两者查询速度差异不过 2 倍,但在插入、删除、修改等可能涉及旋转的操作中,红黑树就领先太多了
假设在约 10 亿 大小的数据中进行查找
AVL树至多需要 30 次出结果- 红黑树至多需要 60 次出结果
但是,区区几十次的差异,对于 CPU 来说几乎无感,反而是频繁的旋转操作令更费时间
记住:红黑树在实际中性能更好,适用性更强;AVL 树适用存储静态、不轻易修改的数据
二、😁红黑树的插入操作
2-1 🌭抽象图
在演示 红黑树 的插入操作时,也需要借助 抽象图 ,此时的 抽象图 不再代表高度,而是代表 黑色节点 的数量
抽象图中已关注的是 黑色节点 的数量
2-2 🍿插入流程
红黑树 的插入流程也和 二叉搜索树 基本一致,先找到合适的位置,然后插入新节点,当节点插入后,需要对颜色进行判断,看看是否需要进行调整
插入流程:
- 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true
- 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走
- 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边
- 根据颜色,判断是否需要进行 染色、旋转 调整高度
整体流程如下(不包括染色调整的具体实现)
cppbool Insert(const std::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); if (parent->_kv.first < kv.first) parent->_right = cur; else parent->_left = cur; cur->_parent = parent; //判断是否需要 染色、旋转 while (parent && parent->_col == RED) { Node* grandfather = parent->_parent; //祖父节点 //...... } return true; }红黑树 如何调整取决于 叔叔 ,即 父亲 的 兄弟节点
并且如果 父亲 为 黑,直接插入就行了,不必调整
如果 父亲为红 并且 叔叔也为红 ,可以只通过 染色 解决当前问题,然后向上走,继续判断是否需要调整
如果 父亲为红 并且 叔叔为黑 或者 叔叔不存在 ,此时需要 旋转 + 染色 ,根据当前节点与父亲的位置关系,选择 单旋 或 双旋 ,值得一提的是 旋转 + 染色 后,不必再向上判断,可以直接结束调整
关于旋转的具体实现,这里不再展开叙述,可以复用 AVL 中的旋转代码,并且最后不需要调整平衡因子
《[C++------lesson29.数据结构进阶------「AVL树」]》
注意: 红黑树的调整可以分为 右半区 和 左半区 两个方向(根据 grandfather 与 parent 的位置关系而定),每个方向中都包含三种情况:单纯染色、单旋+染色、双旋+染色,逐一讲解费时费力,并且两个大方向的代码重复度极高,因此 下面的旋转操作基于 右半区
左半区 的操作和 右半区 基本没啥区别,可以去完整代码中求证
2-3 🧂单纯染色
如果 父亲 为黑色,则不需要调整,不讨论这种情况,下面三种情况基本要求都是:父亲为红
当新节点插入后,如果 叔叔 节点也为 红色 ,那么可以通过将 祖父 节点的黑色素下放给 父亲和叔叔 ,祖父节点 变为 红色 ,这样调整仍可确保 每条路径中的黑色节点数目相同
单次染色还不够,需要从 grandfather 处继续向上判断是否需要 调整 ,单纯染色后,向上判断可能会变成其他情况,这是不确定的,具体情况具体分析
单纯染色 的操作如下:
注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点
修正: 动图中语句修正为 "父亲为红,叔叔也为红,直接染色即可"当 单次染色 结束后,更新 cur 至 grandfather 的位置,并同步更新 parent,继续判断是需要进行 单纯染色 、单旋 + 染色 还是 双旋 + 染色
本质:将公共的黑色下放给两个孩子
代码片段如下(右半分区)
cpp//在右半区操作 Node* uncle = grandfather->_left; //叔叔节点 if (uncle && uncle->_col == RED) { //染色、向上更新即可 grandfather->_col = RED; parent->_col = uncle->_col = BLACK; cur = grandfather; parent = cur->_parent; } else { //此时需要 旋转 + 染色 //...... }叔叔 存在且为 红 很好处理,难搞的是 叔叔 不存在或 叔叔 为 黑 ,需要借助 旋转 降低高度
注意: 此时的五个抽象图,都代表同一个具象图;如果 parent 为空,证明 cur 为根节点,此时需要把根节点置为 黑色,在返回 true 前统一设置即可
c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新是因为,g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束了;如果g就是整棵树的根,再把g变回⿊⾊。
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。
•跟AVL树类似,图0我们展⽰了⼀种具体情况,但是实际中需要这样处理的有很多种情况。
•图1将以上类似的处理进⾏了抽象表达,d/e/f代表每条路径拥有hb个⿊⾊结点的⼦树,a/b代表每条路径拥有hb-1个⿊⾊结点的根为红的⼦树,hb>=0。
•图2/图3/图4,分别展⽰了hb == 0/hb == 1/hb == 2的具体情况组合分析,当hb等于2时,这⾥组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理⽅式⼀样的,变⾊再继续往上处理即可,所以我们只需要看抽象图即可
2-4 🥓左单旋 + 染色
单旋:右右、左左 ,此时在 右半区 ,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲 的 右边 时,可以通过 左单旋 降低高度
如果在左半区,节点位于父亲的左边时,则使用 右单旋 降低高度
在高度降低后,需要使用 染色 确保符合 红黑树 的性质
旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整
左旋转 + 染色 的操作如下:
注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点
显然,旋转 + 染色 后,parent 是一定会被修改为 黑色 的,所以不必再往上判断调整,因为现在已经很符合性质了(即使 parent 的父亲是 红色 ,也不会出现连续的 红色节点)
本质:将 parent 的左孩子托付给 grandfather 后,parent 往上提,并保证不违背性质
代码片段如下(右半分区)
cpp//在右半区操作 Node* uncle = grandfather->_left; //叔叔节点 if (uncle && uncle->_col == RED) { //染色、向上更新即可 //...... } else { //此时需要 旋转 + 染色 if (parent->_right == cur) { //右右,左单旋 ---> parent 被提上去了 RotateL(grandfather); grandfather->_col = RED; parent->_col = BLACK; cur->_col = RED; } else { //右左,右左双旋 ---> cur 被提上去了 //...... } //旋转后,保持平衡,可以结束调整 break; }注意: 这种情况多半是由 单纯染色 转变而来的,所以不同区域的抽象图有不同的情况,必须确保能符合红黑树的性质
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。 分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。
如果p是g的左,c是p的左,那么以g为旋转点进⾏右单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则
如果p是g的右,c是p的右,那么以g为旋转点进⾏左单旋,再把p变⿊,g变红即可。p变成课这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
2-5 🥚右左双旋 + 染色
双旋:右左、左右 ,此时在 右半区 ,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲 的 左边 时,可以通过 右左双旋 降低高度
如果在左半区,节点位于父亲的右边时,则使用 左右双旋 降低高度
在高度降低后,需要使用 染色 确保符合 红黑树 的性质
旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整
右左双旋 + 染色 的操作如下:
注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点
双旋 其实就是两个不同的 单旋 ,不过对象不同而已,先 右旋转 parent ,再 左旋转 grandfather 就是 右左双旋
本质:将 cur 的右孩子托付给 parent,左孩子托付给 grandfather 后,把 cur 往上提即可,并保证不违背 红黑树 的性质
代码片段如下(右半分区)
cppNode* grandfather = parent->_parent; //祖父节点 if (grandfather->_right == parent) { //在右半区操作 Node* uncle = grandfather->_left; //叔叔节点 if (uncle && uncle->_col == RED) { //染色、向上更新即可 //...... } else { //此时需要 旋转 + 染色 if (parent->_right == cur) { //右右,左单旋 ---> parent 被提上去了 //...... } else { //右左,右左双旋 ---> cur 被提上去了 RotateR(parent); RotateL(grandfather); grandfather->_col = RED; parent->_col = RED; cur->_col = BLACK; } //旋转后,保持平衡,可以结束调整 break; }注意: 双旋的情况也可以由 单纯变色 转变而来,同样的,不同区域的抽象图代表不同的含义;对 parent 进行右单旋,对 grandfather 进行左单旋
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。
2-6 🍳具体实现代码
总的来说,红黑树 的插入操作其实比 AVL 树 还要略显简单,画图分析后,确认如何 染色 就行了,下面是插入操作的完整源码(包括左、右单旋)
插入
cppbool Insert(const std::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); if (parent->_kv.first < kv.first) parent->_right = cur; else parent->_left = cur; cur->_parent = parent; //判断是否需要 染色、旋转 while (parent && parent->_col == RED) { Node* grandfather = parent->_parent; //祖父节点 if (grandfather->_right == parent) { //在右半区操作 Node* uncle = grandfather->_left; //叔叔节点 if (uncle && uncle->_col == RED) { //染色、向上更新即可 grandfather->_col = RED; parent->_col = uncle->_col = BLACK; cur = grandfather; parent = cur->_parent; } else { //此时需要 旋转 + 染色 if (parent->_right == cur) { //右右,左单旋 ---> parent 被提上去了 RotateL(grandfather); grandfather->_col = RED; parent->_col = BLACK; cur->_col = RED; } else { //右左,右左双旋 ---> cur 被提上去了 RotateR(parent); RotateL(grandfather); grandfather->_col = RED; parent->_col = RED; cur->_col = BLACK; } //旋转后,保持平衡,可以结束调整 break; } } else { //在左半区操作 Node* uncle = grandfather->_right; //叔叔节点 //同理,进行判断操作 if (uncle && uncle->_col == RED) { //直接染色 grandfather->_col = RED; parent->_col = uncle->_col = BLACK; cur = grandfather; parent = cur->_parent; } else { //需要 旋转 + 染色 if (parent->_left == cur) { //左左,右单旋 ---> parent 被提上去 RotateR(grandfather); grandfather->_col = RED; parent->_col = BLACK; cur->_col = RED; } else { //左右,左右双旋 ---> cur 被提上去 RotateL(parent); RotateR(grandfather); grandfather->_col = RED; parent->_col = RED; cur->_col = BLACK; } break; } } } //再次更新根节点的颜色,避免出问题 _root->_col = BLACK; return true; }左单旋
cpp//左单旋 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; //将 subR 的孩子交给 parent parent->_right = subRL; if (subRL != nullptr) subRL->_parent = parent; //提前保存 parent 的父节点信息 Node* pparent = parent->_parent; //将 parent 变成 subR 的左孩子 subR->_left = parent; parent->_parent = subR; //更新 subR 的父亲 if (pparent == nullptr) { //此时 parent 为根,需要改变根 _root = subR; _root->_parent = nullptr; } else { //根据不同情况进行链接 if (pparent->_right == parent) pparent->_right = subR; else pparent->_left = subR; subR->_parent = pparent; } }右单旋
cpp//右单旋 void RotateR(Node* parent) { //基本原理和左单旋一致 Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR; if (subLR != nullptr) subLR->_parent = parent; Node* pparent = parent->_parent; subL->_right = parent; parent->_parent = subL; if (parent == _root) { _root = subL; _root->_parent = nullptr; } else { if (pparent->_right == parent) pparent->_right = subL; else pparent->_left = subL; subL->_parent = pparent; } }关于左右单旋的详细细节,可以去隔壁 AVL 树的文章查看,红黑树 这里不必阐述
2-6 🧇注意事项及调试技巧
红黑树 这里也涉及很多等式 == 判断,一定要多多注意,不要漏写 =
三叉链 结构,要注意 父指针 的调整
红黑树 的调整情况如下:
- 右半区
右右左单旋右左,右左双旋- 左半区
左左,右单旋左右,左右双旋得益于前面 AVL 树旋转操作的学习,红黑树 这里在编写 旋转 相关代码时,没什么大问题
红黑树 的 DeBug 逻辑与 AVL 树一致,这里额外分享一个 DeBug 技巧:
- 当 随机插入 数据出错时,可以借助文件读写操作,将出错的数据保存下来,然后再次输入,反复进行调试,即可找出 Bug
- 因为是 随机插入 时出现的问题,所以需要保存一下数据样本
关于 红黑树 详细操作可以参考这篇 Blog:《红黑树(C++实现)》
三、😂AVL树 VS 红黑树
AVL 树 和 红黑树 是 平衡二叉搜索树 的两种优秀解决方案,既然两者功能一致,那么它们的实际表现如何呢?可以通过大量随机数插入,得出结果
当然,在切磋之前,需要先验证一下之前写的 红黑树 的正确性
3-1 🥞红黑树的检验
可以借助红黑树 的性质,从下面这三个方面进行检验:
- 验证根节点是否为 黑色节点
- 验证是否出现连续的 红色节点
- 验证每条路径中的 黑色节点 数量是否一致
判断黑色节点数量,需要先获取 基准值
- 简单,先单独遍历一遍,其中的路径,这里选择了最左路径,将这条路径中获取的黑色节点数作为基准值,传给函数判断使用
孩子不一定存在,但父亲一定存在(当前节点为 红色 的情况下)
- 所以当节点为 红色 时,判断父亲是否为黑色,如果不是,则非法!
cpp//合法性检验 bool IsRBTree() const { if (_root->_col != BLACK) { std::cerr << "根节点不是黑色,违反性质二" << std::endl; return false; } //先统计最左路径中的黑色节点数量 int benchMark = 0; //基准值 Node* cur = _root; while (cur) { if (cur->_col == BLACK) benchMark++; cur = cur->_left; } //统计每条路径的黑色节点数量,是否与基准值相同 int blackNodeSum = 0; return _IsRBTree(_root, blackNodeSum, benchMark); } protected: bool _IsRBTree(Node* root, int blackNodeSum, const int benchMark) const { if (root == nullptr) { if (blackNodeSum != benchMark) { std::cerr << "某条路径中的黑色节点数出现异常,违反性质四" << std::endl; return false; } return true; } if (root->_col == BLACK) { blackNodeSum++; } else { //检查当前孩子的父节点是否为 黑节点 if (root->_parent->_col != BLACK) { std::cerr << "某条路径中出现连续的红节点,违反性质三" << std::endl; return true; } } return _IsRBTree(root->_left, blackNodeSum, benchMark) && _IsRBTree(root->_right, blackNodeSum, benchMark); }通过代码插入约
10000个随机数,验证是否是红黑树鉴定为 合法,并且高度有 16,比 AVL 树略高一层(情理之中)
3-2 🧈性能对比
红黑树 不像 AVL 树那样过度自律,其主要优势体现在 插入数据 时的效率之上,可以通过程序对比一下
cppvoid RBTreeTest2() { srand((size_t)time(NULL)); AVLTree<int, int> av; RBTree<int, int> rb; int begin1, begin2, end1, end2, time1 = 0, time2 = 0; int n = 100000; int sum = 0; for (int i = 0; i < n; i++) { int val = (rand() + i) * sum; sum ++; begin1 = clock(); av.Insert(make_pair(val, val)); end1 = clock(); begin2 = clock(); rb.Insert(make_pair(val, val)); end2 = clock(); time1 += (end1 - begin1); time2 += (end2 - begin2); } cout << "插入 " << sum << " 个数据后" << endl; cout << "AVLTree 耗时: " << time1 << "ms" << endl; cout << "RBTree 耗时: " << time2 << "ms" << endl; cout << "=================================" << endl; cout << "AVLree: " << av.IsAVLTree() << " | " << "高度:" << av.getHeight() << endl; cout << "RBTree: " << rb.IsRBTree() << " | " << "高度:" << rb.getHeight() << endl; }此时数据量太小了,还不能体现 红黑树 的价值,还好这次测试,红黑 比 AVL 强
红黑树还是有实力的
红黑树 是 set 和 map 的底层数据结构,在下篇文章中,将会进一步完善 红黑树 ,并用我们自己写的 红黑树 封装 set / map,最后可以和库中的切磋一下~
本文中涉及的源码:《RBTree 博客》
🔥提炼与总结🔥
红黑树是自平衡二叉搜索树,核心价值是通过规则维持平衡,兼顾查找、插入、删除高效性。
1️⃣核心认知:红黑树的本质与核心规则
1. 本质:基于BST扩展,通过红/黑节点标记及规则实现近似平衡,避免退化为线性链表。
2. 核心性质(平衡基石):
① 节点非红即黑;
② 根、叶子(NIL)为黑;
③ 红节点子节点必为黑;
④ 任意节点到叶子路径黑节点数相同。
**3. 特点:**近似平衡减少旋转开销,插入/删除性能优于严格平衡树;查找复杂度O(logn),是Java TreeMap等的常用结构。
2️⃣操作核心:插入操作的逻辑与关键技巧
1. 插入原则:新节点默认染红(减少性质破坏),插入后通过染色或旋转+染色修正平衡。
2. 修正方案:
① 叔节点红:父、叔染黑,祖父染红(单纯染色);
② 叔节点黑:按插入位置选左单旋/右左双旋,配合染色调整。
**3. 实践要点:**先按BST规则插入,调试重点校验5条性质;代码封装旋转/染色方法,规避边界情况
3️⃣对比优势:红黑树与AVL树的核心差异
**1. 平衡标准:**AVL严格平衡(高度差≤1),红黑树近似平衡(黑高一致)。
**2. 性能:**查找AVL略优(高度更矮),但均为O(logn);插入/删除红黑树更优(旋转次数少),高频更新场景优势明显。
**3. 红黑树检验:**需同时满足BST左小右大(结构合法)和5条颜色性质(颜色合法)。
4️⃣核心提炼:红黑树的价值与应用场景
1. 核心逻辑:以"BST结构+颜色约束"为核心,通过"插入染红→校验→修正"实现自平衡,保障操作O(logn)复杂度。
**2. 核心优势:**平衡维护成本与操作效率均衡,适合高频更新、低频查找场景,支持有序性与范围查询。
**3. 应用启示:**体现"近似平衡优于严格平衡"的工程思维,兼顾有序性、高效查找与更新时,是更优选择。
结束语
以上就是我对于【C++】STL 学习------「红黑树」的理解
感谢你的三连支持!!!




























