在上一篇内容里,我们能明显感受到 AVL 树对绝对平衡的极致执念:
任何一个节点的左右子树高度差,严格限制不能超过1。
一旦插入节点打破平衡,就必须立刻通过单旋、双旋强行拉回完美平衡。
但静下心来想一个现实问题:这种近乎苛刻的绝对平衡,真的值得工程中大规模使用吗?
答案其实并不乐观。AVL 树为了维持严格平衡,付出的代价非常大:只要子树高度发生一点变化,就会一路向上更新平衡因子,稍有失衡就要触发旋转。插入、删除带来的旋转次数太频繁,每一次旋转都要修改大量指针、维护父节点关系,CPU 开销不小。
换句话说,AVL 树更像一个理想化的理论模型,追求数学意义上的完美平衡,却忽略了工程上的执行成本。
那有没有一种树,不用绝对平衡、允许轻微失衡,但能把高度控制在合理范围,又能大幅减少旋转次数?
红黑树,就是为了解决这个问题而生的。
它放弃了AVL那种"必须完全等高"的严苛要求,转而采用弱平衡规则:不追求绝对完美,只通过一套颜色约束规则,间接把树的高度控制在最坏也不会差太多的水平。
牺牲一丁点绝对平衡,换来巨大优势:插入、删除时旋转次数极少,绝大多数情况只需改颜色就能搞定,不用频繁动指针做旋转。
工程里看重的从来不是 "数学上最完美",而是综合效率最高 。AVL树适合查询多、几乎不增删的场景;而增删频繁、追求综合性能的工程场景,比如C++的map/set、Java的TreeMap、Linux内核调度,全部清一色选用红黑树。
接下来,我们就从零开始,系统讲解红黑树的五大性质、颜色规则、结构原理,再一步步实现插入调整与旋转逻辑。
目录
[1.1 红黑树的概念](#1.1 红黑树的概念)
[1.2 红黑树的四大核心规则](#1.2 红黑树的四大核心规则)
[1.3 补充:红黑树第五条规则(算法导论标准)](#1.3 补充:红黑树第五条规则(算法导论标准))
[1.4 关于路径的核心理解](#1.4 关于路径的核心理解)
[1.5 红黑树为什么能保证最长路径 ≤ 最短路径的2倍](#1.5 红黑树为什么能保证最长路径 ≤ 最短路径的2倍)
[1.6 红黑树的效率:极高的查找性能](#1.6 红黑树的效率:极高的查找性能)
[2.1 总体结构搭建:红黑树与AVL树的结构差异](#2.1 总体结构搭建:红黑树与AVL树的结构差异)
[2.2 红黑树插入一个值的大致流程](#2.2 红黑树插入一个值的大致流程)
[2.3 情况一:u存在且为红------直接变色](#2.3 情况一:u存在且为红——直接变色)
[2.3.1 c是新增结点](#2.3.1 c是新增结点)
[2.3.2 c不是新增结点的抽象分析](#2.3.2 c不是新增结点的抽象分析)
[2.3.3 c不是新增结点的具体分析](#2.3.3 c不是新增结点的具体分析)
[2.3.3.1 情况一:hb == 0](#2.3.3.1 情况一:hb == 0)
[2.3.3.2情况二:hb == 1](#2.3.3.2情况二:hb == 1)
[2.3.3.3 情况三:hb == 2](#2.3.3.3 情况三:hb == 2)
[2.4 情况二:u不存在 或u存在且为黑 + 直线结构 ------ 变色 + 单旋](#2.4 情况二:u不存在 或u存在且为黑 + 直线结构 —— 变色 + 单旋)
[2.4.1 c是新增结点](#2.4.1 c是新增结点)
[2.4.2 补充 Tips:一个重要结论+不可能出现的混搭情况](#2.4.2 补充 Tips:一个重要结论+不可能出现的混搭情况)
[2.4.3 非新增结点的情况](#2.4.3 非新增结点的情况)
[2.5 情况三:u不存在/存在且为黑+折线结构 ------ 变色 + 双旋](#2.5 情况三:u不存在/存在且为黑+折线结构 —— 变色 + 双旋)
[2.5.1 c是新插入结点](#2.5.1 c是新插入结点)
[2.5.2 c 不是新插入结点](#2.5.2 c 不是新插入结点)
[2.6 红黑树插入代码](#2.6 红黑树插入代码)
[3.1 红黑树验证原理](#3.1 红黑树验证原理)
[3.2 四条规则的验证思路](#3.2 四条规则的验证思路)
[3.3 红黑树验证代码](#3.3 红黑树验证代码)
一、认识红黑树
1.1 红黑树的概念
红黑树本质上首先是一棵二叉搜索树。
和AVL树不一样的是:AVL树靠平衡因子 维护平衡,而红黑树给每个节点多增加了一个颜色标识位,节点颜色只有两种:红色、黑色。
它不靠严格限制左右子树高度差来维持平衡,而是通过约束根到空叶子路径上的节点颜色分布 ,间接保证:任意一条路径的长度,最多不会超过其他路径的2倍 。不用做到AVL那种绝对平衡,属于弱平衡二叉搜索树,但整体高度被牢牢限制,查询效率依旧非常优秀。
1.2 红黑树的四大核心规则
只要一棵二叉搜索树满足下面四条约束,它就是合法的红黑树:
- 每个节点的颜色,只能是红色 或黑色;
- 根节点必须是黑色;
- 若一个节点是红色,那它的左右孩子都必须是黑色。简单理解:整条路径上不能出现连续两个红色节点;
- 对任意一个节点,从它出发,走到所有末端空叶子节点的每条路径上,黑色节点的数量都相等。
只要严格遵守这四条规则,整棵树就会被约束在合理高度范围内,自动达成近似平衡。

1.3 补充:红黑树第五条规则(算法导论标准)
《算法导论》里还额外规定了第五条性质:5. 所有NIL叶子结点(空结点)都是黑色的。
这里要特别分清概念:它说的叶子,不是我们平时说的普通数据叶子节点 ,而是特指NIL空结点 ,也常被称作外部结点。
引入NIL空结点的目的很简单:统一规整整棵树的所有路径,让每一条路径都能完整走到末端空结点,方便用规则四去统计每条路径上的黑色节点数量,定义更严谨、逻辑更自洽。
不过实际手写实现红黑树的时候,我们一般不会额外构造NIL结点,直接把空指针视作隐含的黑色 NIL就行。后续讲解和代码实现里也会默认忽略显性NIL结点,只需要了解这条规则的由来和作用就够了。
1.4 关于路径的核心理解
学红黑树,最容易踩坑的就是路径这个概念,很多人一开始都会理解错。
不少人想当然觉得:树的路径条数 = 传统叶子结点的个数。比如一棵树有4个叶子节点,就误以为只有 4 条路径,这是完全错误的。
正确定义一定要记死:红黑树规则里所说的路径 ,必须一直延伸到**空结点NULL(NIL外部结点)**才算完整。
简单说:从根出发,每走到一个空指针位置 ,就是一条独立完整路径。所以路径总数,等于整棵树里所有NULL空子树的数量,根本不是普通叶子节点的数量。
举个例子:即便只有4个普通叶子结点,但整棵树统计下来一共有9处空结点,那实际就有9条路径,而不是4条。
吃透这一点,后面理解红黑树第四条规则、判断黑色节点数量、理解高度不超两倍限制,就完全不会混乱了。

以上图为例,路径示意图:

1.5 红黑树为什么能保证最长路径 ≤ 最短路径的2倍
根据红黑树第四条规则:从任意节点出发,到所有NULL空节点的每条路径上,黑色节点数量完全相等 ,这个统一的黑色节点层数,我们记作bh(黑高)。
看极端情况就很好理解:
- 最短路径整条路径全是黑色节点,没有任何红色节点,路径长度就等于黑高bh。
- 最长路径受第三条规则限制:不能有连续红色节点。极端排布就是一黑一红交替,每一个黑色节点中间夹一个红色节点,整条路径长度就是2 * bh。
所以不管红黑树实际怎么排布,任意一条根到NULL的路径长度h一定满足:bh≤h≤2×bh这就是红黑树最长路径不会超过最短路径两倍的底层原理。
1.6 红黑树的效率:极高的查找性能
设整棵红黑树一共有N个有效节点,树高为h。
由上面路径约束能推出:2bh−1≤N变形得:bh≤log2(N+1)
又因为 h≤2⋅bh,代入可得:h≤2log2(N+1)
也就是说:红黑树的树高始终维持在 O(logN) 级别,查找、插入、删除的时间复杂度都是稳定的 O(logN)。

红黑树的平衡逻辑,相比 AVL 树要抽象得多。AVL 树是靠高度差来直接控制平衡的,任意节点的左右子树高度差必须严格≤1,一旦失衡立刻旋转修正,是一种非常直观、直接的平衡策略。
而红黑树走的是 "间接路线",它不靠高度差来判断平衡,而是通过四条颜色规则来约束树的结构,间接实现近似平衡:
- 不能有连续的红色节点
- 根节点和所有路径的黑色节点数量保持一致
- 以此保证最长路径不会超过最短路径的 2 倍
两者在理论效率上属于同一档次,增删查改的时间复杂度都是稳定的O(logN)。但在实际工程表现上,红黑树的优势非常明显:因为它不追求AVL那种 "绝对完美的平衡",对结构的控制更宽松,所以在插入相同数量节点时,红黑树需要触发的旋转次数要少得多。
绝大多数失衡场景,红黑树只需要修改节点颜色就能解决,只有极少数情况才需要用到旋转。
而 AVL 树为了维持严格的高度差,增删操作稍微改变树的结构,就可能触发连锁旋转,维护成本高得多。
简单来说:
AVL树是"为了完美平衡,愿意付出更多旋转代价";
红黑树是 "为了减少旋转,接受轻微的不平衡"。
这种取舍,让红黑树在增删频繁的工程场景中,综合效率反而比AVL树更高
二、红黑树的插入
2.1 总体结构搭建:红黑树与AVL树的结构差异
红黑树的结构和AVL树几乎一样 ,唯一的区别就是:每个节点多了一个颜色变量,不再使用平衡因子。
下面是标准的红黑树节点结构:
cpp
using namespace std;
// 颜色枚举:只有红、黑两种
enum Color {
RED,
BLACK
};
template <class K, class V>
class RBTreeNode
{
public:
pair<K, V> _Data; // 存储键值对
RBTreeNode<K, V>* _left; // 左孩子
RBTreeNode<K, V>* _right; // 右孩子
RBTreeNode<K, V>* _parent; // 父节点(三叉链)
Color _col; // 节点颜色(红黑树独有)
// 构造:新节点默认是红色!
RBTreeNode(const pair<K, V>& pir)
: _Data(pir)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{}
};
template <class K, class V>
class RBTree
{
using Node = RBTreeNode<K, V>;
public:
// ... 后面实现插入、旋转、调整 ...
private:
Node* _root = nullptr;
};
2.2 红黑树插入一个值的大致流程
红黑树插入一共分两步:
- 按照二叉搜索树规则找到位置,把节点插进去
- 检查是否违反红黑树规则,如果违反就向上调整
这里最关键、最容易出错的,就是:
新节点到底应该插红色还是黑色?
结论先说死:
- 空树插入:根节点 → 黑色
- 非空树插入:新节点一律 → 红色
为什么非空树插入必须是红色?
我们来对比两种插入方式:
① 如果插入黑色节点
规则四要求:每条路径的黑色节点数量必须完全相同 。你直接插一个黑色节点 → 必定破坏规则四 !这是绝对无法避免的错误,调整起来极其麻烦。
② 如果插入红色节点
只会出现一种问题:如果父亲也是红色,就出现了连续红色 → 破坏规则三 。如果父亲是黑色 → 完全没问题,不用调整!
对比一下:
- 插黑色:必错(破坏规则四)
- 插红色:可能错(只破坏规则三)
显然,插入红色是最优选择。
进一步推导:一旦出错,结构一定是这样的
我们把节点命名固定下来,后面所有情况都用这套称呼:
- cur (c):当前新增的红色节点
- parent (p):父节点
- grandfather (g):祖父节点
- uncle (u):叔叔节点(父亲的兄弟)
当插入红色 cur 引发错误时,一定满足:
- cur 是红色
- parent 也是红色(出现连续红,破坏规则三)
- grandfather 一定是黑色(因为插入前这棵树是合法红黑树,不可能有连续红)
这是所有红黑树插入调整的统一前提。

现在已经确定:新节点cur红 、父节点p红 、祖父节点g黑,这三个节点颜色是完全固定死的。
既然这三者颜色不能变,那整个失衡局面怎么修复、用不用旋转,唯一决定性因素就只剩叔叔节点 u。
所以接下来所有修复逻辑,全部按照叔叔u的状态来划分场景,逐个针对性处理。

2.3 情况一:u存在且为红------直接变色
2.3.1 c是新增结点
已知前提:c 红、p 红、g 黑、u 存在且为红
把p和u改成黑色,g改成红色:
- p、u变黑:解决c和p连续红色 的违规问题;
- 左、右子树所有路径,各自都多了一个黑色节点;
- 再把g变红,刚好抵消上面新增的黑色,整棵子树每条路径的黑色节点总数保持不变,不违反红黑树规则四。

为什么要继续往上更新
g被染成红色后,需要向上继续判断:
- 如果g的父节点还是红色:又出现连续红,必须继续向上调整;
- 如果g的父节点是黑色:没有违规,调整直接结束;
- 如果g已经是整棵树的根:最后把根重新改回黑色即可。
这种情况只变色、不旋转 。不管c在p的左/右孩子,也不管p在g的左/右孩子,变色规则完全一样,不需要区分结构方向。
2.3.2 c不是新增结点的抽象分析
前面只讨论了第一次插入新节点 的场景。实际向上迭代调整时,原来的祖父节点g被染红后,会变成新的待调整节点c,继续往上追溯,这时当前节点已经不是最开始那个新增节点了。
为了通用化、不局限具体某张图,做抽象定义:
- 子树a、b:黑高为bh−1的任意合法红黑子树;
- 子树d、e、f:黑高为bh的任意合法红黑子树。
即便换成这种抽象子树模型,处理逻辑完全不变:依旧把 p、u染黑,g染红 ,再把g 当作新的当前节点,继续向上循环调整。
这个过程可以一直递归向上,直到不再出现连续红节点,或者走到根节点收尾为止。这也体现了情况一的通用性:无论第几轮向上调整,只要满足 u 存在且为红,统一只变色、不旋转。

2.3.3 c不是新增结点的具体分析
2.3.3.1 情况一:hb == 0
这里的hb=0含义:a、b、c、d、e 所有子树全部为空,没有任何多余节点,结构是最极简的基础形态。
此时虽然极简,但规则逻辑完全不变:当前节点x作为6或者15的任意一个孩子,只要满足:自身红、父节点红、祖父节点黑、叔叔节点存在且为红 ,就会触发标准的变色逻辑:父节点、叔叔节点染黑,祖父节点染红,再把祖父节点当作新节点向上继续校验

2.3.3.2情况二:hb == 1
子树可能出现的情况:

黑高hb=1:d、e、f本身都是黑高为1 的合法红黑子树;原本中间节点c是黑色 ,因为在它的子树 a、b中做插入,触发向上调整,c被从黑色染成红色,变成新的待调整节点。
结构与颜色变化逻辑
- a、b 本身为红色节点;
- 在 a、b 四个孩子空位 的任意一处插入新节点,都会触发变色:
- a、b 由红变为黑色
- 上层节点 c 由黑变为红色
- 然后把变红的 c 当作新的当前节点,继续往上递归调整
组合数量推导
- d/e/f每棵子树有4种合法形态(x/y/z/m 四类结构)三者组合:4×4×4=444=64 种
- a、b一共有4个可插入位置
总组合情况:444×4=64×4=256
哪怕结构扩展到 hb=1、子树形态多样、插入位置多,依然完全套用情况一的统一规则:
p、u 染黑 → g 染红 → 把 g 当新节点继续向上更新,只变色、不旋转,256 种分支场景全部用同一套逻辑搞定,不用额外区分。

2.3.3.3 情况三:hb == 2
黑高hb=2:
- d、e、f都是黑高为2的合法红黑树;
- a、b是黑高为1、根节点为红色 的红黑子树。

子树组合规模
- d/e/f 每棵子树的形态数量:256+16种
- 三者自由组合:(256+16)×(256+16)×(256+16)=272×272×272=20123648
再看 a、b:a 和 b 本身是 hb=1、根为红色 的子树;想要达到当前这层状态,在 a 或 b 内部插入节点后,至少要经历两轮变色 + 向上递归调整 才能推演到现在的结构。
把d/e/f庞大组合,再叠加a、b内部各种插入位置、调整路径的所有可能,整体结构组合数量直接达到百亿级别。

哪怕情况复杂到 hb=2、衍生出百亿种具体形态,也不需要逐个枚举、逐个写逻辑 。红黑树情况一的强大之处就在这:只要满足:c 红、p 红、g 黑、u 存在且为红 不管 hb 是 0、1、2 还是更高,也不管子树内部有多少种排列组合,统一一套处理规则:p、u 变黑 → g 变红 → 把 g 当作新 c 继续向上迭代只变色,不旋转,通杀所有层级、所有复杂子树形态。
2.4 情况二:u不存在 或u存在且为黑 + 直线结构 ------ 变色 + 单旋
2.4.1 c是新增结点
依旧沿用统一身份:
- c(cur):新增红色节点
- p(parent):父节点 红色
- g(grandfather):祖父节点 黑色
- u(uncle):不存在 或者 存在但是黑色
- 结构是直线结构:g→p→c 顺向一条链(左左/右右)
处理方法
-
先改色:
- 父节点 p 染黑色
- 祖父节点 g 染红色
-
再单旋:
- 以 g 为旋转轴 ,做一次单旋
- 左左结构:对 g 右旋
- 右右结构:对 g 左旋
- 以 g 为旋转轴 ,做一次单旋
-
把p变黑:直接断掉c、p连续红 的违规,修复规则三;
-
把g变红:平衡整棵子树每条路径的黑色节点总数,不破坏黑高一致的规则四;
-
对g做单旋:把p抬到祖父位置,顶替g的结构地位,整体树恢复近似平衡;
-
旋转+改色完成后,不需要再向上继续追溯调整,整棵子树局部黑高完全合规,红黑树性质全部满足。
- 叔叔 u 为空/黑色,不能像情况一那样只靠变色解决;
- 直线结构适配一次单旋就能摆平;
- 固定套路:先改色,后单旋,处理完直接结束,不用往上迭代。

现在核心矛盾只有一个:c红、p红,出现连续红色,违反红黑树不能连红的规则。
把p改成黑色,直接切断c和p的连续红冲突,立刻修复规则三。
那为什么一定要把g改成红色 ?如果只把p变黑、g不变色,旋转之后,经过g的各条路径上黑色节点数量会变多,破坏规则四------各路径黑节点数量相等 。把g同步染红,就是为了补偿黑高,保证旋转后整棵局部子树的黑高不变,所有路径黑色节点数量依旧均等。
要不要继续向上调整?
不需要往上继续更新。
原因:旋转完成后,原本的 p 上升成为这棵子树的新根,且 p 是黑色。黑色节点不会和它上层父节点形成连续红,同时局部子树黑高完全合规、四条红黑树性质全部满足,调整到此结束。
2.4.2 补充 Tips:一个重要结论+不可能出现的混搭情况
固定前提依旧:c 红、p 红、g 黑,u 不存在 或 u 存在且为黑
结论先记死:
- u 不存在 → c 一定是新增结点
- u 存在且为黑 → c 一定不是新增结点
Tips1:为什么u不存在时,c只能是新增结点?
反证理解:假设c不是新增节点 ,那c一定是从上层祖父节点变色染红上来的(是上一轮调整中g变红后变成新c)。
拿结构举例:如果c不是新增,那它最初原型是黑色节点,只是向上变色流程里被改成了红色。这就会造成:某一条路径上堆积两个黑色节点,而另一条通向NIL的路径只有一个黑色节点,直接违反红黑树第四条:任意路径黑色节点数量相等。

这种结构本身就不合法,所以不可能存在。
因此只要u不存在 ,当前的c只能是刚插进来的新增节点,不可能是向上迭代过来的旧节点。
Tips.2:为什么u存在且为黑的时候,c一定不是新增结点?

和上面一样的道理,如果u存在且为黑,如果c是新插入的结点,那么10->6->3路径上只有一个黑色结点,10->15路径上就会有两个黑色结点,破坏了规则四。
2.4.3 非新增结点的情况
处理方式其实和新增结点是一样的:把父节点p改成黑色,祖父节点g改成红色,再以g为旋转轴,做一次右单旋就行。
简单说下背后逻辑:这时候叔叔节点本身就是黑色,没法再靠单纯变色去维持整棵子树的黑高平衡了,只能靠旋转来重新梳理局部结构。通过一次单旋,把父节点往上提、祖父节点往下落,重新分配黑高,既能彻底解决父子连续红节点的冲突,还能保证这次调整不会再往上蔓延,到这一步就可以收尾,不用继续向上迭代处理了。

2.5 情况三:u不存在/存在且为黑+折线结构 ------ 变色 + 双旋
2.5.1 c是新插入结点
首先说清楚条件哈:还是老样子,c是红色,p是红色,g是黑色,u要么不存在,要么存在但也是黑色。关键区别在于,这时候的结构不是之前那种直线了,而是呈 "折线型"------ 简单说就是 g→p→c 拐了个弯,这种情况下单旋根本解决不了问题,必须得用双旋来调整。
具体方法也不复杂,分三步来:先以p为旋转轴,做一次单旋,把这个折线结构拉直,变成之前情况二的直线结构;接着再以 g 为旋转轴,再做一次单旋;最后一步改颜色,把 c 改成黑色,g 改成红色,这样就搞定了。

现在整体是折线结构,如果直接拿g做单旋,是行不通的。没法同时兼顾两件事:
- 一是消掉c和p的红红连续冲突。
- 二是维持整棵子树的黑高不变。
所以要分两步旋转:
- 先围着p做一次单旋,目的很简单,就是把拐弯的折线结构先拉直,变成我们上一种情况的直线形态;
- 再围着g做第二次单旋,真正把局部树结构重新理顺、重构平衡。
旋转完再配合改色:
- 把c染成黑色,把左右两边的黑色高度还原到插入前的状态;
- 再把g改成红色,用来平衡右侧路径的黑色节点数量,保证调整前后黑高一致。
要不要继续往上处理?
不用。调整完之后,这一小块子树已经完全平衡了,而且 c 成了这片局部的新根、还是黑色,不会再和上层节点形成新的红红相连,到这里就可以结束了。
2.5.2 c 不是新插入结点
处理方法操作和新插入结点完全一样:先以p为轴单旋把折线拉直,再以g为轴做第二次单旋,最后把c变黑、g变红就行。
背景与条件说明
这种c并不是刚插进来的新节点,它原本其实是黑色的。只是在它自己的子树里发生了插入,先触发了情况一(叔叔 u 为红),经过一轮变色逻辑后,它被染成了红色,接着一路向上回溯调整,才在当前这一层又形成了红红冲突。
即便来源不一样,满足的条件还是那套:c 红、p 红、g 黑,u 要么不存在要么为黑,整体依旧是折线结构,所以直接套用同一套双旋 + 改色逻辑就可以处理。

因为叔叔节点本身就是黑色,单靠变色已经撑不住整体的黑高平衡了。再加上现在是折线型结构,只用一次单旋根本没法把结构理顺。
这种情况下,只能靠双旋来同时搞定结构重构和颜色合规的问题。先绕着p旋转一次,把折线强行拉成直线;再绕g做第二次旋转,把整段子树的拓扑结构彻底重整好。
旋转完再做颜色修正:把c染成黑色,g改成红色。这样一来c就成了这棵局部子树的新根,既保住了各条路径的黑色节点总数不变,也彻底解决了父子连续红节点的违规问题。
至于要不要继续往上递归调整?完全不用。调整结束后,这片子树已经完全符合红黑树所有规则,而且新的子树根节点 c 是黑色,不会再和上层节点产生红红相连的隐患,问题到这就终止了,不会再往上蔓延。
2.6 红黑树插入代码
cpp
// 右单旋:以 cur 为轴右旋
void RotateR(Node* cur) {
Node* leftChild = cur->_left;
Node* leftRight = cur->_left->_right;
Node* parent = cur->_parent;
// 如果旋的是根,更新根
if (cur == _root)
_root = leftChild;
// 右旋核心:左孩子上位,cur 变右孩子
leftChild->_right = cur;
cur->_parent = leftChild;
// 处理被"挤下来"的子树
if (leftRight)
leftRight->_parent = cur;
cur->_left = leftRight;
// 接上原来的父节点
leftChild->_parent = parent;
if (parent) {
if (parent->_Date.first > leftChild->_Date.first)
parent->_left = leftChild;
else
parent->_right = leftChild;
}
}
// 左单旋:以 cur 为轴左旋
void RotateL(Node* cur) {
Node* rightChild = cur->_right;
Node* rightLeft = cur->_right->_left;
Node* parent = cur->_parent;
if (cur == _root)
_root = rightChild;
// 左旋核心:右孩子上位,cur 变左孩子
rightChild->_left = cur;
cur->_parent = rightChild;
// 处理被挤下来的子树
cur->_right = rightLeft;
if (rightLeft)
rightLeft->_parent = cur;
// 接上原来的父节点
rightChild->_parent = parent;
if (parent) {
if (parent->_Date.first > rightChild->_Date.first)
parent->_left = rightChild;
else
parent->_right = rightChild;
}
}
// 右左双旋 RL:先右旋右孩子,再左旋自己
void RotateRL(Node* cur) {
RotateL(cur->_right);
RotateL(cur);
}
// 左右双旋 LR:先左旋左孩子,再右旋自己
void RotateLR(Node* cur) {
RotateL(cur->_left);
RotateR(cur);
}
// 红黑树插入(核心)
bool Insert(const pair<K, V>& kv) {
// 空树直接插根,必须黑色
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 1. 先按二叉搜索树规则找到插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur) {
if (kv.first < cur->_Date.first) {
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_Date.first) {
parent = cur;
cur = cur->_right;
}
else {
return false; // key 重复,不插入
}
}
// 2. 创建新节点,默认红色(最合理)
cur = new Node(kv);
cur->_col = RED;
cur->_parent = parent;
// 链接到父节点
if (kv.first < parent->_Date.first)
parent->_left = cur;
else
parent->_right = cur;
// 3. 开始调整:解决 连续红色 问题
while (parent && parent->_col == RED) {
Node* grandpa = parent->_parent;
// 父节点在祖父左边:LL / LR
if (grandpa->_left == parent) {
Node* uncle = grandpa->_right;
// 情况一:叔叔存在 + 红色 → 只变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
// 继续向上处理
cur = grandpa;
parent = cur->_parent;
}
// 情况二、三:叔叔不存在 / 黑色 → 旋转
else {
// LL 直线 → 右单旋
if (parent->_left == cur) {
RotateR(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
// LR 折线 → 左右双旋
else {
RotateLR(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
// 父节点在祖父右边:RR / RL
else {
Node* uncle = grandpa->_left;
// 情况一:叔叔红 → 变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
// 叔叔黑 → 旋转
else {
// RR 直线 → 左单旋
if (parent->_right == cur) {
RotateL(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
// RL 折线 → 右左双旋
else {
RotateRL(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
// 最后强制根节点为黑色
_root->_col = BLACK;
return true;
}
三、红黑树的验证
3.1 红黑树验证原理
很多人会想:直接算最长路径和最短路径,判断最长不超过最短的两倍行不行?其实不可行。
就算最长路径≤2倍最短路径,也不代表红黑树颜色规则是合法的。
现在看着没问题,一旦继续插入节点,立刻就会出bug。
所以最稳妥、最标准的做法只有一个:严格检查红黑树四条核心规则,四条都满足,就一定合法,也一定满足路径长度要求。
3.2 四条规则的验证思路
- 规则 1:节点颜色只能是红或黑我们用枚举类实现,天然就保证了不会出现其他颜色,这条不用额外检查。
- 规则 2:根节点必须是黑色直接判断一下 _root->_col == BLACK 就行,非常简单。
- 规则 3:不能有连续的红色节点(红红冲突)直接查红色节点的孩子有点麻烦,因为孩子可能为空、有两个。反过来查父节点就方便多了:遍历的时候,如果当前节点是红色,直接看它的父亲是不是红色,一秒就能判断是否冲突。
- 规则 4:每条路径上的黑色节点数量必须相同用前序遍历,在递归里用形参记录从根到当前节点的黑色节点个数。遇到黑色节点就 blackNum++,走到空节点(NIL)就代表一条路径走完了。拿第一条路径的黑高作为标准,后面所有路径和它比较是否相等就行。

3.3 红黑树验证代码
cpp
// 对外接口:判断整棵红黑树是否合法
bool IsBalanceTree() {
// 规则2:根节点必须是黑色
if (_root && _root->_col == RED)
return false;
// 空树也算合法
if (_root == nullptr)
return true;
// 选取最左路径的黑色节点数,作为黑高参考值
int blackHeightRef = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK)
blackHeightRef++;
cur = cur->_left;
}
// 递归检查:规则3(连续红)+ 规则4(黑高一致)
return check(_root, 0, blackHeightRef);
}
// 递归校验核心
bool check(const Node* cur, int blackNum, int blackHeightRef) {
// 走到空节点(NIL),判断当前路径黑高是否正确
if (cur == nullptr) {
return blackNum == blackHeightRef;
}
// 规则3:检查连续红色节点
// 当前节点是红色 → 父节点不能是红色
if (cur->_col == RED && cur->_parent->_col == RED) {
cout << "违反规则:连续红节点!路径:"
<< cur->_parent->_Date.first << " -> " << cur->_Date.first << endl;
return false;
}
// 遇到黑色节点,计数+1
if (cur->_col == BLACK) {
blackNum++;
}
// 递归检查左右子树
return check(cur->_left, blackNum, blackHeightRef) &&
check(cur->_right, blackNum, blackHeightRef);
}
四、红黑树完整代码
cpp
#pragma once
#include <iostream>
#include <cassert>
#include <vector>
using namespace std;
// 颜色枚举
enum Color {
RED,
BLACK
};
// 红黑树节点
template <class K, class V>
class RBTreeNode
{
public:
pair<K, V> _Date;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
RBTreeNode(const pair<K, V>& pir)
: _Date(pir)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED) // 新节点默认红色
{}
};
template <class K, class V>
class RBTree
{
using Node = RBTreeNode<K, V>;
public:
// 右单旋
void RotateR(Node* cur) {
Node* SubL = cur->_left;
Node* SubLR = cur->_left->_right;
Node* pParent = cur->_parent;
if (cur == _root)
_root = SubL;
SubL->_right = cur;
cur->_parent = SubL;
if (SubLR)
SubLR->_parent = cur;
cur->_left = SubLR;
SubL->_parent = pParent;
if (pParent) {
if (pParent->_Date.first > SubL->_Date.first)
pParent->_left = SubL;
else
pParent->_right = SubL;
}
}
// 左单旋
void RotateL(Node* cur) {
Node* SubR = cur->_right;
Node* SubRL = cur->_right->_left;
Node* pParent = cur->_parent;
if (cur == _root)
_root = SubR;
SubR->_left = cur;
cur->_parent = SubR;
cur->_right = SubRL;
if (SubRL)
SubRL->_parent = cur;
SubR->_parent = pParent;
if (pParent) {
if (pParent->_Date.first > SubR->_Date.first)
pParent->_left = SubR;
else
pParent->_right = SubR;
}
}
// 右左双旋 RL(先右旋右孩子,再左旋自己)
void RotateRL(Node* cur) {
RotateR(cur->_right);
RotateL(cur);
}
// 左右双旋 LR(先左旋左孩子,再右旋自己)
void RotateLR(Node* cur) {
RotateL(cur->_left);
RotateR(cur);
}
// 插入(核心)
bool Insert(const pair<K, V> pir) {
// 空树直接插根
if (_root == nullptr) {
_root = new Node(pir);
_root->_col = BLACK;
return true;
}
// BST 查找插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur) {
if (cur->_Date.first > pir.first) {
parent = cur;
cur = cur->_left;
}
else if (cur->_Date.first < pir.first) {
parent = cur;
cur = cur->_right;
}
else {
return false; // key 重复
}
}
// 新建节点(默认红色)
cur = new Node(pir);
cur->_col = RED;
cur->_parent = parent;
if (parent->_Date.first > cur->_Date.first)
parent->_left = cur;
else
parent->_right = cur;
// 调整:解决连续红
while (parent && parent->_col == RED) {
Node* grandpa = parent->_parent;
// 父节点在祖父左边
if (grandpa->_left == parent) {
Node* uncle = grandpa->_right;
// 情况一:叔叔存在且为红 → 变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
// 情况二、三:叔叔不存在/为黑 → 旋转
else {
if (parent->_left == cur) {
RotateR(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
else {
RotateLR(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
// 父节点在祖父右边
else {
Node* uncle = grandpa->_left;
// 情况一:叔叔红 → 变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
// 叔叔黑 → 旋转
else {
if (parent->_right == cur) {
RotateL(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
else {
RotateRL(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
// 根永远保持黑色
_root->_col = BLACK;
return true;
}
// 中序遍历(验证二叉搜索树)
void Inorder() {
inorder(_root);
cout << endl;
}
// 节点数量
int Size() {
return size(_root);
}
// 树高度
int Height() {
return height(_root);
}
// 查找
Node* Find(const pair<K, V> pir) {
Node* cur = _root;
while (cur) {
if (cur->_Date.first > pir.first)
cur = cur->_left;
else if (cur->_Date.first < pir.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
// 红黑树合法性校验
bool IsBalanceTree() {
// 规则2:根必须为黑
if (_root && _root->_col == RED)
return false;
if (_root == nullptr)
return true;
// 取最左路径作为黑高参考
int Referance = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK)
Referance++;
cur = cur->_left;
}
return check(_root, 0, Referance);
}
private:
// 递归检查:连续红 + 黑高一致
bool check(const Node* cur, int BlackNum, int Referance) {
// 走到空节点,判断当前路径黑高是否正确
if (cur == nullptr) {
return BlackNum == Referance;
}
// 规则3:不能有连续红
if (cur->_col == RED) {
if (cur->_parent->_col == RED) {
cout << "出现连续红节点:"
<< cur->_parent->_Date.first << " -> "
<< cur->_Date.first << endl;
return false;
}
}
// 黑色节点计数
if (cur->_col == BLACK)
BlackNum++;
// 递归左右
return check(cur->_left, BlackNum, Referance) &&
check(cur->_right, BlackNum, Referance);
}
// 求节点数
int size(const Node* root) {
if (root == nullptr) return 0;
return size(root->_left) + size(root->_right) + 1;
}
// 求树高
int height(const Node* root) {
if (root == nullptr) return 0;
int lh = height(root->_left);
int rh = height(root->_right);
return max(lh, rh) + 1;
}
// 中序
void inorder(const Node* root) {
if (root == nullptr) return;
inorder(root->_left);
cout << "key:" << root->_Date.first
<< " value:" << root->_Date.second << endl;
inorder(root->_right);
}
private:
Node* _root = nullptr;
};