系列文章目录
提示:这里是系列文章的专栏
提示:以下是文章目录哦!
文章目录
目录
[1. 红黑树的定义](#1. 红黑树的定义)
[2. 红黑树的五大核心规则(重点)](#2. 红黑树的五大核心规则(重点))
[3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题)](#3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题))
[4. 红黑树的效率分析](#4. 红黑树的效率分析)
[图 1:插入更多红色节点后的状态](#图 1:插入更多红色节点后的状态)
[图 2:插入新节点 6、22 后的状态](#图 2:插入新节点 6、22 后的状态)
[图 3:插入大量红色节点后的 "满二叉树" 状态](#图 3:插入大量红色节点后的 “满二叉树” 状态)
[图 4:全黑的 "理想初始状态"(无红节点)](#图 4:全黑的 “理想初始状态”(无红节点))
[图 5:插入红色节点后的状态(带 NIL 节点)](#图 5:插入红色节点后的状态(带 NIL 节点))
[1.1 模块 :颜色枚举定义](#1.1 模块 :颜色枚举定义)
[1.2 模块 :红黑树节点结构体定义](#1.2 模块 :红黑树节点结构体定义)
[1.3 模块 :红黑树类的框架定义](#1.3 模块 :红黑树类的框架定义)
[2.1 插入操作的整体流程](#2.1 插入操作的整体流程)
[2.2 插入的三种核心情况(带图示讲解)](#2.2 插入的三种核心情况(带图示讲解))
[情况 1:叔叔节点存在且为红色(变色即可解决)](#情况 1:叔叔节点存在且为红色(变色即可解决))
[图 1:入门级场景(hb=0,最简单的首次插入)](#图 1:入门级场景(hb=0,最简单的首次插入))
[图 2:基础场景(单次插入 + 递归起点)](#图 2:基础场景(单次插入 + 递归起点))
[图 3:进阶场景(hb=1,黑高为 1 的子树扩展)](#图 3:进阶场景(hb=1,黑高为 1 的子树扩展))
[图 4:终极场景(hb=2,多层递归的复杂场景)](#图 4:终极场景(hb=2,多层递归的复杂场景))
[情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色)](#情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色))
[图1:最简场景(首次插入触发情况 2)](#图1:最简场景(首次插入触发情况 2))
[图 2:通用场景(递归过程中触发情况 2)](#图 2:通用场景(递归过程中触发情况 2))
[图 3:带黑高说明的详细场景](#图 3:带黑高说明的详细场景)
[情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色)](#情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色))
[图1:最简场景(首次插入触发情况 3)](#图1:最简场景(首次插入触发情况 3))
[2.3 插入操作的完整代码实现](#2.3 插入操作的完整代码实现)
[模块 1:插入函数入口 & 空树处理](#模块 1:插入函数入口 & 空树处理)
[模块 2:BST 查找插入位置](#模块 2:BST 查找插入位置)
[模块 3:创建新节点并挂到父节点上](#模块 3:创建新节点并挂到父节点上)
[模块 4:核心:插入后红黑树规则调整(三种情况)](#模块 4:核心:插入后红黑树规则调整(三种情况))
[模块 5:收尾:保证根节点为黑色](#模块 5:收尾:保证根节点为黑色)
[1. 红黑树的查找操作](#1. 红黑树的查找操作)
[2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?)](#2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?))
[2.1 递归检查函数 Check](#2.1 递归检查函数 Check)
[2.2 对外接口 IsBalance](#2.2 对外接口 IsBalance)
[2.3 完整代码如下](#2.3 完整代码如下)
[3. 删除操作](#3. 删除操作)
前言
提示:这里可以添加本文要记录的大概内容:
前面我们已经系统学习了普通二叉搜索树(BST)和 AVL 树两种自平衡结构。我们知道,BST 在极端情况下会退化成链表,导致查询效率骤降;而 AVL 树通过严格的高度差规则,实现了完美平衡,但也带来了频繁旋转的额外开销
而红黑树,正是为了在「平衡效率」和「维护成本」之间找到最佳平衡点而诞生的。它不追求像 AVL 树那样的绝对平衡,而是通过一套巧妙的颜色规则,保证树的最长路径不超过最短路径的两倍,从而实现了接近 O (logN) 的稳定效率,同时大幅降低了旋转调整的频率。正因如此,它被选为了 C++ STL 中std::map和std::set的底层实现
这篇博客,我们将从红黑树的核心规则入手,一步步拆解它的平衡原理、插入时的变色与旋转调整,并最终给出完整的代码实现,帮你彻底搞懂这一经典数据结构
提示:以下是本篇文章正文内容
一、为什么需要红黑树?
回顾普通二叉搜索树的痛点:极端情况下会退化成链表,时间复杂度从 O(logN) 变成 O(N)
引出平衡二叉树的概念:AVL 树 vs 红黑树
- AVL 树:严格平衡(左右子树高度差≤1),插入删除需要频繁旋转,适合读多写少场景
- 红黑树 :近似平衡 ,通过 "颜色规则" 实现弱平衡,旋转次数更少,性能更稳定,因此被广泛应用(如
C++ STL的map/set、Java 的TreeMap、Linux 内核进程调度)
二、红黑树的核心概念与规则
1. 红黑树的定义
红黑树是一种自平衡的二叉搜索树,每个节点额外存储一个 "颜色标记(红 / 黑)",通过一套颜色规则,保证树的整体平衡,避免退化成链表
2. 红黑树的五大核心规则(重点)
- 规则 1:每个节点,非黑即红
- 规则 2:根节点必须是黑色
- 规则 3:所有叶子节点(NIL 空节点)都是黑色
- 规则 4:红色节点的两个子节点,必须都是黑色(不能出现两个连续的红色节点)
- 规则 5:从任意节点出发,到其所有叶子节点的路径上,黑色节点的数量必须相同(黑高相同)
3. 红黑树如何保证最长路径不超过最短路径的 2 倍?(关键问题)
- 最短路径:全是黑色节点**,准确点就是路径上红色节点数量最少的那条路径** (黑高为
h)- 最长路径:红黑交替(黑高同样为
h,路径长度为2h)- 结论:最长路径长度 ≤ 2 × 最短路径长度,因此树的高度始终维持在
O(logN),保证了查找效率
4. 红黑树的效率分析
- 查找:
O(logN),和 AVL 树一样 - 插入 / 删除:
O(logN),但平均旋转次数比 AVL 树少,写操作性能更优 - 适用场景:读、写操作都频繁的场景,是工程中更常用的平衡树实现
我们来尝试推导一下:
先设两个未知数:
N:红黑树中实际节点的总数(不算 NIL 空叶子节点)h:红黑树的黑高(根节点到 NIL 叶子的路径上,黑色节点的数量,所有路径黑高都相等)
我们要证明的是:红黑树的高度始终是 O (logN),因此增删查改的时间复杂度都是 O (logN)

1. 公式 1:2^h - 1 ≤ N(下界,最少节点数)
- 这是红黑树节点数的理论下限 : 当红黑树里没有任何红色节点时,它就变成了一棵完全由黑色节点组成的满二叉树
- 一棵黑高为
h的满二叉树,最少需要2^h - 1个节点(比如 h=3 时,节点数是 7) - 而你的红黑树实际节点数
N,一定大于等于这个最小值,所以2^h - 1 ≤ N
2. 公式 2:N < 2^(2h) - 1(上界,最多节点数)
- 这是红黑树节点数的理论上限 : 红黑树的规则限制了红色节点的数量,它的最长路径也只能是 "黑红交替" 的形式
- 黑高为
h时,路径上最多有h个黑色节点,再插入h个红色节点(黑红交替),路径总长度就是2h - 这时候,整棵树的节点数最多接近一棵高度为
2h的满二叉树,而满二叉树的节点数是2^(2h) - 1,所以N < 2^(2h) - 1
- 黑高为
3. 把两个不等式结合起来
我们把两个式子合在一起:

我们对两边同时取对数(以 2 为底):
- 对左边:
2^h ≤ N + 1→h ≤ log₂(N+1)→h ≈ logN - 对右边:
N + 1 < 2^(2h)→log₂(N+1) < 2h→h > (log₂(N+1))/2
所以最终得到:

红黑树的增删查改操作,都需要从根节点走到叶子节点,时间复杂度和树的高度成正比
- 我们已经证明,树的黑高
h本身就是O(logN) - 最坏情况下,我们需要走的是最长路径,也就是
2h(黑红交替的路径) - 但
2h依然是O(logN)级别(常数系数不影响时间复杂度的阶)
所以,红黑树的增删查改操作,最坏时间复杂度依然是 O (logN)
5.红黑树实际例子理解

图 1:插入更多红色节点后的状态
这棵树里,红色节点变多了,我们来验证规则和路径:
- 规则检查:
- 无连续红节点:30 (红) 的父 18 (黑)、子 25 (黑)/40 (黑);15 (红) 的父 10 (黑);35/50 (红) 的父 40 (黑),全部满足规则 4。
- 黑高检查:所有路径的黑高都是 3(根 18、10/30/40 分支的黑节点数)。
- 路径长度对比 :
- 最短路径:18→10→NIL(路径长度 2,黑高 3)
- 最长路径:18→30→40→35→NIL(路径长度 4,黑高 3)
- 此时最长路径 = 2 × 最短路径,刚好触达了红黑树的路径上限。

图 2:插入新节点 6、22 后的状态
这是插入新节点后的中间状态,我们可以看到调整的痕迹:
- 节点 6 (红)、22 (红) 的父节点都是黑色,依然满足规则 4。
- 黑高仍然保持一致:所有路径的黑高都是 3。
- 路径变化 :
- 新增路径 18→10→6→NIL(路径长度 3,黑高 3)
- 新增路径 18→30→25→22→NIL(路径长度 4,黑高 3)
- 最长路径依然是最短路径的 2 倍,没有超过上限。

图 3:插入大量红色节点后的 "满二叉树" 状态
这是红黑树路径的极端情况,也是理解 "最长路径≤2 倍最短路径" 的关键例子:
- 规则检查:
- 红色节点 10、30 的子节点都是黑色;所有叶子节点 1、8、12、16、20、22、35、50 都是红色,父节点都是黑色,无连续红节点。
- 黑高检查:所有路径的黑高都是 3(18→6/15/25/40 分支,黑节点数一致)。
- 路径长度对比 :
- 最短路径:18→10→6→NIL(路径长度 3,黑高 3)
- 最长路径:18→10→6→1→NIL(路径长度 4,黑高 3)
- 所有路径中,最长路径始终没有超过最短路径的 2 倍,完美体现了红黑树的平衡特性。

图 4:全黑的 "理想初始状态"(无红节点)
这是一棵完全由黑色节点组成的二叉树,是理解红黑树路径的基础
- 它天然满足所有规则:根黑、无红节点、黑高处处相等
- 路径特点:所有路径的长度完全相同,最短路径 = 最长路径 = 3(节点数)
- 这是 "最短路径" 的极端情况:路径上全是黑色节点,黑高为 3

图 5:插入红色节点后的状态(带 NIL 节点)
规则 3 提到的 NIL 黑色叶子节点,是标准的红黑树表示法。
- 插入了红色节点 15 和 40,满足规则 4:红色节点的父 / 子节点都是黑色。
- 我们来算一下黑高:
- 根节点 18 → 左路径:18 (黑)→10 (黑)→15 (红)→NIL (黑),黑高为 3(18、10、NIL)
- 根节点 18 → 右路径:18 (黑)→30 (黑)→40 (红)→NIL (黑),黑高同样为 3
- 路径长度对比 :
- 最短路径:18→10→NIL(黑高 3,路径长度 2)
- 最长路径:18→10→15→NIL(黑高 3,路径长度 3)
- 此时最长路径 = 1.5 × 最短路径,已经体现了 "红节点延长路径但不增加黑高" 的特点,以及,最长路径长度 ≤ 2 × 最短路径长度
-------------------------------------------------红黑树代码实现讲解-----------------------------------------------------
三、红黑树的核心实现
1.红黑树主体的实现
1.1 模块 :颜色枚举定义

- 用
enum定义了两种颜色,方便给节点标记颜色 - 为什么用枚举?一是代码可读性更好,二是避免直接用数字(0/1)导致写错
1.2 模块 :红黑树节点结构体定义

- 为什么要有
_parent指针?- 红黑树插入后,需要向上回溯父节点、叔叔节点、祖父节点来调整颜色和结构,没有父指针就做不到
- 为什么新节点默认是红色?
- 如果设为黑色,会直接破坏红黑树的「黑高一致」规则,需要从根节点开始全局调整,成本极高;设为红色只会破坏「不能连续红节点」规则,调整成本低很多
1.3 模块 :红黑树类的框架定义

typedef是为了简化代码,不然每次都要写RBTreeNode<K, V>,太麻烦了_root是整个树的入口,初始为nullptr表示空树
总代码如下:
cpp
#include <iostream>
#include <utility> // 包含pair头文件,用于键值对
// 模块1:颜色枚举定义
enum Colour
{
RED, // 红色节点
BLACK // 黑色节点
};
// 模块2:红黑树节点结构体定义
template<class K, class V>
struct RBTreeNode
{
// 存储键值对数据
std::pair<K, V> _kv;
// 左右孩子指针 + 父节点指针
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
// 节点颜色
Colour _col;
// 节点构造函数
RBTreeNode(const std::pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED) // 新节点默认红色
{}
};
// 模块3:红黑树类框架
template<class K, class V>
class RBTree
{
private:
// 简化节点类型名
typedef RBTreeNode<K, V> Node;
public:
// 后面会在这里实现:插入、查找、验证、旋转等函数
private:
// 红黑树根节点
Node* _root = nullptr;
};
// 测试用例(空树测试)
int main()
{
// 创建一个空的红黑树
RBTree<int, int> tree;
std::cout << "红黑树初始化完成,根节点地址:" << &tree << std::endl;
std::cout << "当前根节点是否为空:" << (tree._root == nullptr ? "是" : "否") << std::endl;
return 0;
}
2.红黑树插入功能的实现
2.1 插入操作的整体流程
- 按照普通二叉搜索树的规则,找到合适的位置插入新节点(默认红色)
- 插入后,判断是否破坏了红黑树的规则(主要是规则 4:不能连续红节点)
- 根据父节点、叔叔节点的颜色,分三种情况进行调整(变色 / 旋转 + 变色)
2.2 插入的三种核心情况(带图示讲解)
情况 1:叔叔节点存在且为红色(变色即可解决)
- 场景:父节点红,叔叔节点红,祖父节点黑
- 调整方法:父节点变黑,叔叔节点变黑,祖父节点变红;然后把祖父节点当成新节点,向上递归调整
- 特点:不需要旋转,仅通过变色解决问题
eg1.特殊情况:

在这张图里,我们先把关键节点的身份标清楚:
x:新插入的节点(默认红色,红黑树的规则)p:x的父节点(Parent)u:p的兄弟节点,也就是x的叔叔节点(Uncle)g:p的父节点,也就是x的祖父节点(Grandparent)
因为新节点x是红色,父节点p也是红色,直接违反了红黑树的规则 4:不能有连续的红色节点。 我们的目标是:在不破坏其他规则的前提下,修正这个冲突
情况 1 的核心解法,就是变色三步骤,图里的箭头已经标出来了:
- 父节点
p变黑 :解决了x和p连续红节点的问题 - 叔叔节点
u变黑 :因为父和叔原本都是红色,一起变黑,能保证从祖父节点g到所有叶子的黑高不变 - 祖父节点
g变红 :把p和u变黑带来的 "额外黑节点" 抵消掉,维持黑高平衡
为什么要把祖父节点当成新节点,向上递归?
调整完之后,g变成了红色,而它的父节点(比如图里的节点 18)可能也是红色,就会出现新的连续红节点问题。 所以我们需要:
- 把
g当成新的 "冲突节点",继续向上检查父、叔、祖父节点 - 重复变色 / 旋转逻辑,直到没有冲突,或者走到根节点为止
eg2.一般情况:

图 1:入门级场景(hb=0,最简单的首次插入)
这张图是情况 1 的「Hello World」版本,帮你建立最基础的认知。
场景描述
hb=0:所有子节点都是空 NIL 节点,黑高为 0- 初始树:祖父
g=10(黑),父p=6(红),叔u=15(红) - 操作:在父节点
p下插入新节点x(默认红色),触发连续红节点冲突
调整过程
- 插入红色节点
x,此时p和x都是红色,违反规则 4 - 父
p变黑、叔u变黑,祖父g变红 - 此时
g变成红色,需要作为新的冲突节点,继续向上检查
关键结论
- 无论新节点
x是父节点的左孩子还是右孩子,只要满足「父红、叔红、祖父黑」,处理逻辑完全一致 - 变色的核心是:父和叔同时变黑,抵消了祖父变红带来的黑高变化,所有路径的黑高依然相等

图 2:基础场景(单次插入 + 递归起点)
这张图分上下两部分,帮你理解「首次插入」和「递归过程中」的两种情况 1 触发场景。
上半部分:首次插入触发情况 1
- 和图 2 逻辑完全一致,就是更通用的版本。
- 新节点
x插入后,父p和叔u都是红色,触发变色调整。 - 调整后祖父
g变红,作为新节点向上递归
下半部分:递归过程中触发情况 1
- 注意文字说明:
c不是新增节点,c是之前的g - 这里的
x不是刚插入的节点,而是上一轮调整后变红的祖父节点,现在向上检查时,再次遇到了「父红、叔红、祖父黑」的情况 - 处理逻辑和上半部分完全一样,重复「父叔变黑、祖父变红」的步骤,继续向上递归
关键结论
- 情况 1 不是一次性操作,它可以在递归过程中连续触发,直到遇到非红的叔叔节点,或者走到根节点

图 3:进阶场景(hb=1,黑高为 1 的子树扩展)
这张图帮你理解:叔叔节点下面的子树结构,完全不影响情况 1 的处理逻辑。
场景描述
hb=1:叔叔节点u下面的子树d/e/f,是黑高为 1 的合法红黑树(比如左上角的 x/y/z/m 四种结构)。- 初始状态:祖父
g=10(黑),父p=6(红),叔u=15(红),父节点下的x=3(上一轮调整后变红的节点)。
调整过程
- 在父节点
p的子树中插入新节点,触发连续红节点冲突。 - 父
p变黑、叔u变黑,祖父g变红,g成为新的冲突节点向上递归。
关键点
- 叔叔节点的子树可以是任意黑高为 1 的红黑树,组合数高达 256 种,但处理逻辑完全不变。
- 变色操作只会影响父、叔、祖父三个节点的颜色,子树的结构和黑高不会被破坏。

图 4:终极场景(hb=2,多层递归的复杂场景)
这张图是情况 1 的「终极形态」,展示了树更深、黑高更高时的处理逻辑
场景描述
hb=2:叔叔节点u下面的子树d/e/f,是黑高为 2 的红黑树;父节点p下面的a/b,是黑高为 1 的红黑树- 这种场景通常是多次递归调整后出现的,组合数高达百亿级,说明情况 1 在复杂树中也会频繁触发
调整过程
- 在父节点
p的子树中插入新节点,触发冲突 - 第一次变色调整:父
p变黑、叔u变黑、祖父g变红,g成为新节点向上递归 - 递归过程中,可能再次遇到父红、叔红的情况,再次触发情况 1,继续变色调整
关键结论
- 无论树的层级多深、结构多复杂,只要满足「父红、叔红、祖父黑」,就可以用情况 1 的变色方法解决问题
- 递归向上调整,是情况 1 的灵魂,它能把冲突从叶子节点一直传递到根节点,最终被完全解决
情况 2:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的左孩子(同侧单旋 + 变色)
- 场景:父节点红,叔叔节点黑,新节点和父节点在同一侧(父左,新左)
- 调整方法:对祖父节点进行右单旋 ;父节点变黑,祖父节点变红

图1:最简场景(首次插入触发情况 2)
1. 初始状态
- 祖父节点
g=10(黑) - 父节点
p=6(红,是g的左孩子) - 叔叔节点
u不存在(空 NIL,相当于黑色) - 新节点
c=3(红,是p的左孩子)
此时问题:p和c都是红色,违反了红黑树「不能连续红节点」的规则。
2. 调整步骤(右单旋 + 变色)
- 对祖父节点
g做右单旋 :- 把
p=6变成这棵子树的新根 g=10变成p的右孩子p原来的右孩子(如果有的话),过继给g当左孩子(这张图里没有,所以省略)
- 把
- 变色 :
- 新根
p=6变黑 - 原来的祖父节点
g=10变红
- 新根
3. 调整后结果
- 新节点
c=3(红)的父节点p=6(黑),不再是连续红节点,规则 4 被修复 - 从这棵子树往上看,黑高和调整前完全一致,规则 5 没有被破坏
图 2:通用场景(递归过程中触发情况 2)
这张图是情况 2 的通用版本,展示了叔叔节点u存在(但为黑)、且子树有结构的情况
1. 初始状态
- 祖父节点
g=10(黑) - 父节点
p=6(红,左孩子) - 叔叔节点
u=15(黑,右孩子) p下面的节点x=3(红,是上一轮调整后变红的节点,现在作为冲突节点c)p的子树a/b,u的子树d/e/f,都是合法的红黑树
2. 调整过程
- 插入新节点,导致
x=3变红,和父节点p=6形成连续红节点冲突 - 对祖父节点
g=10做右单旋:p=6成为新根,g=10变成p的右孩子p原来的右孩子(如果有的话),过继给g当左孩子(这张图里是d)
- 变色:
- 新根
p=6变黑 - 原来的祖父节点
g=10变红
- 新根
3. 关键结论
- 叔叔节点只要是黑色,不管它下面的子树是什么结构,处理逻辑都是一样的:单旋 + 变色
- 调整后,所有路径的黑高依然保持一致,没有破坏红黑树的规则
图 3:带黑高说明的详细场景
这张图是情况 2 的进阶说明,帮你理解「为什么单旋 + 变色不会破坏黑高」
1. 初始状态(黑高 hb=1)
- 祖父节点
g=10(黑),父p=6(红),叔u=15(黑) p的子树a/b,u的子树d/e/f,都是黑高为 1 的红黑树x=3是上一轮调整后变红的节点,作为冲突节点c
2. 调整过程
- 对
g=10做右单旋,p=6成为新根 p=6变黑,g=10变红- 此时:
p的分支:黑高 + 1(p从红变黑)g的分支:黑高不变(g从黑变红,但它的父节点现在是变黑的p,整体黑高和原来一致)
3. 为什么不需要向上递归?
调整后,新根p变成了黑色,它和它的父节点(上一层的节点)不会形成连续红节点冲突,所以情况 2 调整完成后,不需要再向上递归,直接结束即可。
情况 3:叔叔节点不存在 / 为黑,且新节点是父节点的左孩子、父节点是祖父节点的右孩子(双旋 + 变色)
- 场景:父节点红,叔叔节点黑,新节点和父节点在不同侧(父左,新右)
- 调整方法:先对父节点进行左单旋 ,转化为情况 2;再对祖父节点进行右单旋,然后变色
- 核心:双旋的本质是把 "拐弯" 的结构拉成 "直线",再用单旋解决
同理,父节点是祖父节点右孩子的镜像场景,逻辑完全对称,一并讲解即可

图1:最简场景(首次插入触发情况 3)
1. 初始状态
- 祖父节点
g=10(黑) - 父节点
p=6(红,是g的左孩子) - 叔叔节点
u不存在(空 NIL,相当于黑色) - 新节点
c=8(红,是p的右孩子)
此时的问题:
p=6和c=8都是红色,违反了「不能连续红节点」的规则- 而且
c和p在不同侧,没法直接对g做单旋,必须先把结构 "掰直"
2. 调整步骤(双旋 + 变色)
-
第一步:对父节点
p做左单旋- 把
c=8旋转上去,变成p原来的位置 p=6变成c的左孩子- 此时结构变成:
g=10的左孩子是c=8,c的左孩子是p=6 - 这一步的目的,是把 "拐弯" 的结构,变成和情况 2 一样的 "直线" 结构
- 把
-
第二步:对祖父节点
g做右单旋- 把
c=8旋转上去,变成这棵子树的新根 g=10变成c的右孩子
- 把
-
第三步:变色
- 新根
c=8变黑 - 原来的祖父节点
g=10变红
- 新根
3. 调整后结果
- 没有连续的红色节点,规则 4 被修复
- 所有路径的黑高和调整前一致,规则 5 没有被破坏
- 新根
c=8是黑色,不会和上层节点形成新的冲突,调整直接结束
图2:通用场景(带子树的递归情况)
这张图是情况 3 的通用版本,展示了叔叔节点存在(但为黑)、且子树有结构的情况。
1. 初始状态
- 祖父节点
g=10(黑),父节点p=6(红,左孩子),叔叔节点u=15(黑,右孩子) p的右孩子x=8是冲突节点(上一轮调整后变红的节点),x的子树是a/bp的左孩子是d,u的子树是e/f
2. 调整过程
- 插入新节点,导致
x=8变红,和父节点p=6形成连续红节点冲突,且x是p的右孩子(异侧) - 第一步:对父节点
p做左单旋x=8变成p的位置,p=6变成x的左孩子x原来的子树a/b,变成p的右孩子
- 第二步:对祖父节点
g=10做右单旋x=8变成新根,g=10变成x的右孩子g原来的左孩子(空),变成x的右孩子
- 第三步:变色
- 新根
x=8变黑 - 原来的祖父节点
g=10变红
- 新根
3. 关键结论
- 叔叔节点只要是黑色,不管它下面的子树是什么结构,处理逻辑都是一样的:先对父节点单旋,再对祖父节点反方向单旋,最后变色
- 调整后,所有路径的黑高依然保持一致,且新根为黑色,不需要向上递归
图3:带黑高说明的详细场景
这张图是情况 3 的进阶说明,帮你理解双旋过程中子树的变化和黑高的一致性。
1. 初始状态(黑高 hb=1)
- 祖父
g=10(黑),父p=6(红),叔u=15(黑) p的左孩子d(黑),右孩子x=8(黑),x的子树a/b(黑高为 1)- 插入新节点后,
x=8变红,触发异侧冲突
2. 调整过程
- 对父节点
p做左单旋:x=8上移,p=6成为其左孩子,a/b成为p的右孩子 - 对祖父节点
g=10做右单旋:x=8成为新根,g=10成为其右孩子 - 变色:
x=8变黑,g=10变红
3. 为什么不需要向上递归?
调整后,新根 x=8 是黑色,它和上层父节点不会形成连续红节点冲突,所以情况 3 调整完成后,不需要再向上递归,直接结束即可。
-------------------------------------------------红黑树插入功能实现-----------------------------------------------------
2.3 插入操作的完整代码实现
模块 1:插入函数入口 & 空树处理

空树时,直接创建根节点,并且必须设为黑色(红黑树规则 2)
模块 2:BST 查找插入位置

和普通二叉搜索树的插入逻辑完全一致,找到父节点位置,同时判断是否重复
模块 3:创建新节点并挂到父节点上

- 新节点默认红色:如果设为黑色,会直接破坏黑高规则,需要全局调整,成本极高。
- 挂到父节点后,设置好
_parent指针,为后续向上回溯调整做准备。
模块 4:核心:插入后红黑树规则调整(三种情况)
cpp
// 4. 插入后调整红黑树规则,循环向上处理,直到父节点为黑色(无冲突)
while (parent && parent->_col == RED)
{
// 父节点是红色,说明一定存在祖父节点(根节点是黑色)
Node* grandfather = parent->_parent;
// 情况分支1:父节点是祖父节点的左孩子
if (parent == grandfather->_left)
{
// 找到叔叔节点(父节点的兄弟节点,即祖父的右孩子)
Node* uncle = grandfather->_right;
// -------------------- 情况1:叔叔节点存在且为红色(仅变色) --------------------
if (uncle && uncle->_col == RED)
{
// 父节点变黑,叔叔节点变黑,祖父节点变红
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 把祖父节点当成新的冲突节点,向上继续调整
cur = grandfather;
parent = cur->_parent;
}
else
{
// -------------------- 情况2:叔叔节点为黑/不存在,且cur和parent同侧(单旋+变色) --------------------
if (cur == parent->_left)
{
// 对祖父节点做右单旋
RotateRight(grandfather);
// 父节点变黑,祖父节点变红
parent->_col = BLACK;
grandfather->_col = RED;
}
// -------------------- 情况3:叔叔节点为黑/不存在,且cur和parent异侧(双旋+变色) --------------------
else
{
// 第一步:对父节点做左单旋,把结构掰直
RotateLeft(parent);
// 第二步:对祖父节点做右单旋
RotateRight(grandfather);
// 新节点cur变黑,祖父节点变红
cur->_col = BLACK;
grandfather->_col = RED;
}
// 情况2/3调整完成后,不会再向上产生冲突,直接break结束循环
break;
}
}
// 情况分支2:父节点是祖父节点的右孩子(上面的镜像场景)
else
{
// 找到叔叔节点(父节点的兄弟节点,即祖父的左孩子)
Node* uncle = grandfather->_left;
// -------------------- 情况1:叔叔节点存在且为红色(仅变色) --------------------
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 向上继续调整
cur = grandfather;
parent = cur->_parent;
}
else
{
// -------------------- 情况2:叔叔节点为黑/不存在,且cur和parent同侧(单旋+变色) --------------------
if (cur == parent->_right)
{
// 对祖父节点做左单旋
RotateLeft(grandfather);
// 父节点变黑,祖父节点变红
parent->_col = BLACK;
grandfather->_col = RED;
}
// -------------------- 情况3:叔叔节点为黑/不存在,且cur和parent异侧(双旋+变色) --------------------
else
{
// 第一步:对父节点做右单旋,把结构掰直
RotateRight(parent);
// 第二步:对祖父节点做左单旋
RotateLeft(grandfather);
// 新节点cur变黑,祖父节点变红
cur->_col = BLACK;
grandfather->_col = RED;
}
// 情况2/3调整完成,直接break
break;
}
}
}
- 循环条件:只要父节点是红色,就存在连续红节点冲突,必须调整
- 核心逻辑:分「父为左孩子 / 父为右孩子」两大分支,每个分支下处理三种情况
- 情况 1:叔叔为红,仅变色,向上递归
- 情况 2/3:叔叔为黑,单旋 / 双旋 + 变色,调整完成直接结束
模块 5:收尾:保证根节点为黑色

调整过程中根节点可能被设为红色,最后强制设为黑色,确保符合红黑树规则 2
完整插入代码
cpp
#include <iostream>
#include <utility>
using namespace std;
// 颜色枚举
enum Colour
{
RED,
BLACK
};
// 红黑树节点结构
template<class K, class V>
struct RBTreeNode
{
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{}
};
// 红黑树类
template<class K, class V>
class RBTree
{
private:
typedef RBTreeNode<K, V> Node;
public:
// 插入函数
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 1. 按BST找到插入位置
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; // 键重复
}
}
// 2. 创建新节点并挂到父节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
{
parent->_right = cur;
}
else
{
parent->_left = cur;
}
cur->_parent = parent;
// 3. 调整红黑树规则
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
// 父为左孩子,叔叔为祖父的右孩子
Node* uncle = grandfather->_right;
// 情况1:叔叔存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况2:同侧(cur是parent的左孩子),单旋
if (cur == parent->_left)
{
RotateRight(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
// 情况3:异侧(cur是parent的右孩子),双旋
else
{
RotateLeft(parent);
RotateRight(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
// 父为右孩子,叔叔为祖父的左孩子
Node* uncle = grandfather->_left;
// 情况1:叔叔存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
// 情况2:同侧(cur是parent的右孩子),单旋
if (cur == parent->_right)
{
RotateLeft(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
// 情况3:异侧(cur是parent的左孩子),双旋
else
{
RotateRight(parent);
RotateLeft(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
// 强制根节点为黑色
_root->_col = BLACK;
return true;
}
private:
// 右单旋(和AVL树的旋转逻辑一致)
void RotateRight(Node* parent)
{
Node* leftChild = parent->_left;
Node* rightChildOfLeft = leftChild->_right;
parent->_left = rightChildOfLeft;
if (rightChildOfLeft)
{
rightChildOfLeft->_parent = parent;
}
leftChild->_right = parent;
leftChild->_parent = parent->_parent;
if (parent == _root)
{
_root = leftChild;
}
else
{
if (parent == parent->_parent->_left)
{
parent->_parent->_left = leftChild;
}
else
{
parent->_parent->_right = leftChild;
}
}
parent->_parent = leftChild;
}
// 左单旋(和AVL树的旋转逻辑一致)
void RotateLeft(Node* parent)
{
Node* rightChild = parent->_right;
Node* leftChildOfRight = rightChild->_left;
parent->_right = leftChildOfRight;
if (leftChildOfRight)
{
leftChildOfRight->_parent = parent;
}
rightChild->_left = parent;
rightChild->_parent = parent->_parent;
if (parent == _root)
{
_root = rightChild;
}
else
{
if (parent == parent->_parent->_left)
{
parent->_parent->_left = rightChild;
}
else
{
parent->_parent->_right = rightChild;
}
}
parent->_parent = rightChild;
}
private:
Node* _root = nullptr;
};
// 测试代码
int main()
{
RBTree<int, int> tree;
tree.Insert({10, 10});
tree.Insert({6, 6});
tree.Insert({15, 15});
tree.Insert({3, 3});
tree.Insert({8, 8});
tree.Insert({12, 12});
tree.Insert({18, 18});
cout << "红黑树插入完成" << endl;
return 0;
}
四、红黑树的其他基础操作
1. 红黑树的查找操作
- 和普通二叉搜索树完全一致,递归 / 迭代实现都可以,简单带过,不过多赘述

2. 红黑树的验证(如何判断一棵树是不是合法的红黑树?)
- 验证是否满足二叉搜索树的性质(中序遍历是否有序)
- 验证根节点是否为黑色
- 验证是否存在连续的红色节点
- 验证所有路径的黑高是否一致
- 可以写一个验证函数,配合插入操作的测试用例,验证你的实现是否正确

这张图很清晰的可以看出我们这里采用前序遍历
2.1 递归检查函数 Check
cpp
// 递归检查红黑树是否合法的核心函数
// root:当前检查的节点
// blackNum:当前路径上已统计的黑色节点数量
// refNum:参考黑高(以最左路径的黑高为标准)
template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum)
{
// 1. 递归终止条件:走到空节点(一条路径结束)
if (root == nullptr)
{
// 路径结束时,检查当前路径的黑高是否等于参考黑高
if (refNum != blackNum)
{
cout << "存在黑色结点的数量不相等的路径" << endl;
return false;
}
// 黑高一致,这条路径合法
return true;
}
// 2. 检查规则4:不能存在连续的红色节点
// 如果当前节点是红色,且父节点也是红色 → 违规
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << " 存在连续的红色结点" << endl;
return false;
}
// 3. 统计当前路径的黑色节点数量
if (root->_col == BLACK)
{
blackNum++;
}
// 4. 递归检查左子树和右子树,只有左右都合法,才返回true
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
Check 递归函数:核心验证逻辑
① 路径黑高检查

当递归走到空节点时,代表一条路径遍历完成。此时会对比这条路径的黑高 blackNum 和参考黑高 refNum,如果不一致,说明违反了红黑树规则 5
② 连续红节点检查

红黑树规则 4 禁止两个连续的红色节点。这里通过检查当前节点和它父节点的颜色,直接发现违规
③ 统计黑高并递归

如果当前节点是黑色,就把 blackNum 加 1,然后递归检查左右子树。只有左右子树都合法,才会返回 true
2.2 对外接口 IsBalance
cpp
// 对外接口:判断整个红黑树是否合法
template<class K, class V>
bool RBTree<K, V>::IsBalance()
{
// 情况1:空树也是合法的红黑树
if (_root == nullptr)
{
return true;
}
// 情况2:检查规则2:根节点必须是黑色
if (_root->_col == RED)
{
cout << "根节点不是黑色,违规" << endl;
return false;
}
// 情况3:计算参考黑高refNum(以最左路径的黑高为标准)
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
refNum++;
}
cur = cur->_left;
}
// 调用递归检查函数,验证整棵树
return Check(_root, 0, refNum);
}
IsBalance 函数:验证的入口
它负责做三件事:
- 空树检查 :空树天然合法,直接返回
true。 - 根节点检查:红黑树规则 2 强制根节点为黑色,否则直接违规。
- 计算参考黑高:从根节点一直往左走,统计最左路径上的黑色节点数量,作为后续所有路径的标准。
2.3 完整代码如下
cpp
template<class K, class V>
class RBTree
{
private:
typedef RBTreeNode<K, V> Node;
// 递归检查函数
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
if (refNum != blackNum)
{
cout << "存在黑色结点的数量不相等的路径" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << root->_kv.first << " 存在连续的红色结点" << endl;
return false;
}
if (root->_col == BLACK)
{
blackNum++;
}
return Check(root->_left, blackNum, refNum)
&& Check(root->_right, blackNum, refNum);
}
public:
// 对外接口
bool IsBalance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
cout << "根节点不是黑色,违规" << endl;
return false;
}
int refNum = 0;
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
refNum++;
}
cur = cur->_left;
}
return Check(_root, 0, refNum);
}
// 其他成员函数(Insert、Find等)
// ...
private:
Node* _root = nullptr;
};
3. 删除操作
- 红黑树的删除逻辑比插入复杂很多,这里就不展开来讲解了,有兴趣的可以自行了解:
- 先回顾 BST 的删除逻辑(叶子节点、单孩子节点、双孩子节点)
- 删除后,处理 "双黑节点" 的调整问题
- 分四种情况讲解双黑节点的修复(兄弟节点为黑 / 红,侄子节点颜色不同)
五、总结与拓展
- 红黑树的核心思想:用颜色规则约束,以少量的调整(变色 / 旋转)换取近似平衡
- 红黑树 vs AVL 树的对比表格
- 红黑树的实际应用场景(STL、Linux 内核等)
- 学习建议:先理解 BST,再学 AVL 树的旋转,最后再啃红黑树的调整逻辑,配合画图理解三种插入情况
