红黑树完全指南:从核心原理到插入验证全实现

在上一篇内容里,我们能明显感受到 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. 每个节点的颜色,只能是红色黑色
  2. 根节点必须是黑色
  3. 若一个节点是红色,那它的左右孩子都必须是黑色。简单理解:整条路径上不能出现连续两个红色节点
  4. 对任意一个节点,从它出发,走到所有末端空叶子节点的每条路径上,黑色节点的数量都相等

只要严格遵守这四条规则,整棵树就会被约束在合理高度范围内,自动达成近似平衡。

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 红黑树插入一个值的大致流程

红黑树插入一共分两步:

  1. 按照二叉搜索树规则找到位置,把节点插进去
  2. 检查是否违反红黑树规则,如果违反就向上调整

这里最关键、最容易出错的,就是:

新节点到底应该插红色还是黑色?

结论先说死:

  1. 空树插入:根节点 → 黑色
  2. 非空树插入:新节点一律 → 红色

为什么非空树插入必须是红色?

我们来对比两种插入方式:

① 如果插入黑色节点

规则四要求:每条路径的黑色节点数量必须完全相同 。你直接插一个黑色节点 → 必定破坏规则四 !这是绝对无法避免的错误,调整起来极其麻烦。

② 如果插入红色节点

只会出现一种问题:如果父亲也是红色,就出现了连续红色 → 破坏规则三 。如果父亲是黑色 → 完全没问题,不用调整!

对比一下:

  • 插黑色:必错(破坏规则四)
  • 插红色:可能错(只破坏规则三)

显然,插入红色是最优选择


进一步推导:一旦出错,结构一定是这样的

我们把节点命名固定下来,后面所有情况都用这套称呼:

  • cur (c):当前新增的红色节点
  • parent (p):父节点
  • grandfather (g):祖父节点
  • uncle (u):叔叔节点(父亲的兄弟)

当插入红色 cur 引发错误时,一定满足:

  1. cur 是红色
  2. parent 也是红色(出现连续红,破坏规则三)
  3. 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被染成红色后,需要向上继续判断:

  1. 如果g的父节点还是红色:又出现连续红,必须继续向上调整;
  2. 如果g的父节点是黑色:没有违规,调整直接结束;
  3. 如果g已经是整棵树的根:最后把根重新改回黑色即可。

这种情况只变色、不旋转 。不管cp的左/右孩子,也不管pg的左/右孩子,变色规则完全一样,不需要区分结构方向。

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被从黑色染成红色,变成新的待调整节点。

结构与颜色变化逻辑

  1. a、b 本身为红色节点;
  2. 在 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:

  1. d、e、f都是黑高为2的合法红黑树;
  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 顺向一条链(左左/右右)

处理方法

  1. 先改色:

    • 父节点 p 染黑色
    • 祖父节点 g 染红色
  2. 再单旋:

    • g 为旋转轴 ,做一次单旋
      • 左左结构:对 g 右旋
      • 右右结构:对 g 左旋
  3. 把p变黑:直接断掉c、p连续红 的违规,修复规则三;

  4. 把g变红:平衡整棵子树每条路径的黑色节点总数,不破坏黑高一致的规则四;

  5. 对g做单旋:把p抬到祖父位置,顶替g的结构地位,整体树恢复近似平衡;

  6. 旋转+改色完成后,不需要再向上继续追溯调整,整棵子树局部黑高完全合规,红黑树性质全部满足。

  • 叔叔 u 为空/黑色,不能像情况一那样只靠变色解决;
  • 直线结构适配一次单旋就能摆平;
  • 固定套路:先改色,后单旋,处理完直接结束,不用往上迭代。

现在核心矛盾只有一个:c红、p红,出现连续红色,违反红黑树不能连红的规则。

p改成黑色,直接切断c和p的连续红冲突,立刻修复规则三。

那为什么一定要把g改成红色 ?如果只把p变黑、g不变色,旋转之后,经过g的各条路径上黑色节点数量会变多,破坏规则四------各路径黑节点数量相等 。把g同步染红,就是为了补偿黑高,保证旋转后整棵局部子树的黑高不变,所有路径黑色节点数量依旧均等。

要不要继续向上调整?

不需要往上继续更新。

原因:旋转完成后,原本的 p 上升成为这棵子树的新根,且 p 是黑色。黑色节点不会和它上层父节点形成连续红,同时局部子树黑高完全合规、四条红黑树性质全部满足,调整到此结束。

2.4.2 补充 Tips:一个重要结论+不可能出现的混搭情况

固定前提依旧:c 红、p 红、g 黑,u 不存在 或 u 存在且为黑

结论先记死:

  1. u 不存在 → c 一定是新增结点
  2. 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的红红连续冲突。
  • 二是维持整棵子树的黑高不变。

所以要分两步旋转:

  1. 先围着p做一次单旋,目的很简单,就是把拐弯的折线结构先拉直,变成我们上一种情况的直线形态;
  2. 再围着g做第二次单旋,真正把局部树结构重新理顺、重构平衡。

旋转完再配合改色:

  1. 把c染成黑色,把左右两边的黑色高度还原到插入前的状态;
  2. 再把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;
};
相关推荐
yu85939581 小时前
基于 QT5.7.0 的八线激光雷达点云聚类实现
开发语言·qt·聚类
号码认证服务1 小时前
客户看到来电显示公司名会更愿意接听吗?企业号码认证提升ROI
服务器·网络·c++·经验分享·智能手机·云计算·php
yoyo_zzm1 小时前
汇编到PHP:五大编程语言核心特性全解析
开发语言·汇编·php
.ZGR.2 小时前
线程池相关知识及并发统计案例实现
java·开发语言
拉拉拉拉拉拉拉马2 小时前
Windsurf 最新版进阶讲解:从 Cascade 到 Devin Local,重新理解 AI 编程工作流
人工智能·算法
流年如夢2 小时前
初入C++
开发语言·c++
Mr_pyx2 小时前
面试题记录
jvm·数据结构·算法·spring·mybatis
zzzsde2 小时前
【Linux】线程同步和互斥(1):线程互斥与加锁实现
linux·运维·服务器·开发语言·算法
yoyo_zzm2 小时前
编程语言大比拼:C++到PHP全解析
开发语言·c++·php