
目录
[1 红黑树概念](#1 红黑树概念)
[1.1 红黑树四条硬性规则](#1.1 红黑树四条硬性规则)
[1.2 为什么最长路径 ≤ 2 × 最短路径](#1.2 为什么最长路径 ≤ 2 × 最短路径)
[1.3 AVL 树 vs 红黑树](#1.3 AVL 树 vs 红黑树)
[2 红黑树结点定义](#2 红黑树结点定义)
[2.1 旋转函数说明](#2.1 旋转函数说明)
[3 红黑树插入完整逻辑](#3 红黑树插入完整逻辑)
[场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转)](#场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转))
[场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色](#场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色)
[场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色](#场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色)
[完整 Insert 插入源码](#完整 Insert 插入源码)
[4 红黑树查找](#4 红黑树查找)
[5 红黑树校验函数(调试必写)](#5 红黑树校验函数(调试必写))
[6 关于红黑树删除](#6 关于红黑树删除)
本节学习目标
- 掌握红黑树 4 条核心规则,理解 NIL 外部叶子结点概念
- 理解红黑树 "最长路径不超过最短路径 2 倍" 的原理,对比 AVL 树
- 掌握红黑树结点结构:
_kv键值对、左右孩子、_parent父指针、颜色枚举 - 掌握红黑树插入完整流程:BST 插入、新增结点默认红色;分三大 case 处理:叔叔红 (仅变色)、叔叔黑 / 不存在单旋变色、叔叔黑 / 不存在双旋变色
- 看懂红黑树的左单旋、右单旋(和 AVL 旋转逻辑一致,没有平衡因子)
- 手写红黑树校验函数:校验根颜色、禁止连续红结点、所有路径黑色结点数目相等
- 对比 AVL 树,理解红黑树优势:旋转次数更少,STL
map/set底层就是红黑树;了解删除复杂,课件不实现删除
1 红黑树概念
红黑树是一棵二叉搜索树,每个结点多一个颜色标记,红色或者黑色。通过颜色约束间接实现近似平衡。
性质保证:任意一条从根到 NIL 空叶子结点的路径,最长路径长度不会超过最短路径的 2 倍。
1.1 红黑树四条硬性规则
- 每个结点只能是红色 或者 黑色。
- 根结点必须是黑色。
- 不能出现连续的红色结点:如果一个结点是红色,那么它的两个直接孩子必须是黑色。(父红则子不能红)
- 对于任意结点,该结点到达所有 NIL(空外部叶子结点)的全部路径上,黑色结点的数量完全相等。
补充 NIL 外部结点说明: 《算法导论》中提到所有 NIL 叶子(外部空结点)是黑色。课件实现中代码不专门创建 NIL 结点,把
nullptr等价看作黑色 NIL 结点。NIL 只是逻辑概念,代码不实例化对象。



1.2 为什么最长路径 ≤ 2 × 最短路径
设黑色高度 bh:从某结点向下走到 NIL,路径上面黑色结点数量。
- 最短路径:全部都是黑色结点,路径长度 =
bh。 - 根据规则 3,不能连续红色结点;极端最长路径:红黑交替,一红一黑 ,路径长度 =
2*bh。
所以:
bh ≤ h ≤ 2*bh。 结点数量 N,时间复杂度:增删查 \(O(logN)\),最坏 2logN。
1.3 AVL 树 vs 红黑树
表格
| AVL 树 | 红黑树 | |
|---|---|---|
| 平衡约束 | 严格平衡,左右子树高度差绝对值≤1 | 颜色约束近似平衡,最长路径≤2 倍最短路径 |
| 平衡控制 | 平衡因子 bf,记录高度差 | 结点红、黑颜色标记 |
| 旋转次数 | 插入删除触发旋转频繁 | 旋转少,大部分情况只做变色 |
| 查找性能 | 理论略快 | 略低,同一复杂度级别 \(O(logN)\) |
| 工程使用 | 极少手写,标准库不使用 | STL map/set底层实现,工业界主流 |
AVL 追求严格高度平衡;红黑树牺牲一点点查找性能,换取更少旋转,综合工程表现更好。
2 红黑树结点定义
cpp
//结点颜色枚举
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
{
typedef RBTreeNode<K,V> Node;
private:
Node* _root = nullptr;
public:
//接口省略
};
✨重点:新插入结点默认设置为 RED 红色。 如果新增结点设置黑色,直接破坏规则 4(每条路径黑色数量相等),修复代价巨大;新增红色,只会可能违反规则 3(连续红)。
2.1 旋转函数说明
红黑树的左单旋
RotateL、右单旋RotateR和 AVL 树旋转代码完全一样!红黑树没有平衡因子,旋转之后不用维护 bf,只需要修改颜色。 旋转核心要点:
- 修改父子孩子指针
- 更新所有结点的
_parent父指针 - 如果旋转结点是根,更新
_root。
cpp
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if(subLR != nullptr)
subLR->_parent = parent;
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if(ppNode == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if(parent == ppNode->_left)
ppNode->_left = subL;
else
ppNode->_right = subL;
subL->_parent = ppNode;
}
}
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if(subRL != nullptr)
subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if(ppNode == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if(parent == ppNode->_left)
ppNode->_left = subR;
else
ppNode->_right = subR;
subR->_parent = ppNode;
}
}
3 红黑树插入完整逻辑
插入步骤总览
- 按照普通 BST 二叉搜索树规则查找位置插入新结点;key 重复返回 false 插入失败。
- 空树:直接 new 结点,根强制设置为 BLACK 黑色,结束。
- 非空树:新结点
cur默认红色。 - 如果父结点
parent是黑色:没有违反任何红黑规则,直接插入结束。 - 如果父结点
parent是红色:违反规则 3(连续红色)。 此时祖父grandfather(g)一定是黑色。接下来看叔叔结点 uncle (u),分三大场景处理。
符号约定:
cur(c):当前结点(新增结点,或者向上迭代上来的结点)parent(p):cur 的父亲grandfather(g):cur 的祖父,p 的父亲,p 为红,g 必然黑色uncle(u):p 的兄弟,cur 的叔叔结点
场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转)





条件:p红,g黑,u红 处理逻辑:
- parent (p) 变黑;uncle (u) 变黑;
- grandfather (g) 变红;
- 把
g赋值给cur,继续向上循环迭代处理; - 循环结束之后,最后强制把根结点置为黑色。
原理:p、u 变黑,两条子路径黑色计数 + 1;g 变红抵消增加的黑色计数,子树黑色高度不变。但是 g 变红,g 和 g 的父结点有可能形成连续红色,必须继续向上处理。
该场景:不管 p 是 g 的左还是右,cur 是 p 左还是右,逻辑完全一样,只变色,不旋转。
场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色

两种子情况:
- LL:p 是 g 左孩子,cur 是 p 左孩子 → 右单旋 RotateR (g)
- 旋转完成,p 变黑;g 变红;处理完毕,break 不再向上循环。
- RR:p 是 g 右孩子,cur 是 p 右孩子 → 左单旋 RotateL (g)
- 旋转完成,p 变黑;g 变红;处理完毕,break。
旋转之后子树黑色高度不变,上层不会出现连续红色,不需要继续向上。
场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色

- LR:p 是 g 左孩子,cur 是 p 右孩子:
- 先对 p 做左单旋
RotateL(p);再对 g 做右单旋RotateR(g); - cur 变黑;g 变红;break 结束。
- 先对 p 做左单旋
- RL:p 是 g 右孩子,cur 是 p 左孩子:
- 先对 p 做右单旋
RotateR(p);再对 g 做左单旋RotateL(g); - cur 变黑;g 变红;break 结束。
- 先对 p 做右单旋
双旋完成后,cur 成为这棵局部子树根,黑色;g 变红,黑色高度不变,停止向上迭代。
完整 Insert 插入源码
cpp
bool Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK; //根必须黑色
return true;
}
//BST查找插入位置
Node* parent = nullptr;
Node* cur = _root;
while(cur != nullptr)
{
if(cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if(cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
//key重复
return false;
}
}
//创建新结点,默认RED红色
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 != nullptr && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//父是红,祖父一定是黑
if(parent == grandfather->_left)
{
//父亲是祖父的左孩子,叔叔是祖父的右孩子
Node* uncle = grandfather->_right;
if(uncle != nullptr && uncle->_col == RED)
{
//【场景1:叔叔红色,只变色】
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
//g作为新cur继续向上
cur = grandfather;
parent = cur->_parent;
}
else
{
//【场景2/3:叔叔为空或者黑色,旋转+变色】
if(cur == parent->_left)
{
//LL 右单旋
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
//LR 双旋:先左后右
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; //旋转处理完毕,退出循环
}
}
else
{
//父亲是祖父的右孩子,叔叔是祖父的左孩子
Node* uncle = grandfather->_left;
if(uncle != nullptr && uncle->_col == RED)
{
//场景1:叔叔红色,变色
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
//叔叔空/黑色,旋转变色
if(cur == parent->_right)
{
//RR左单旋
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
//RL双旋,先右后左
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
//循环结束,强制根结点黑色
_root->_col = BLACK;
return true;
}
4 红黑树查找
复用二叉搜索树查找逻辑,时间复杂度 \(O(logN)\)
cpp
Node* Find(const K& key)
{
Node* cur = _root;
while(cur != nullptr)
{
if(cur->_kv.first < key)
{
cur = cur->_right;
}
else if(cur->_kv.first > key)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
5 红黑树校验函数(调试必写)

❗错误思路:只判断最长路径 ≤2 倍最短路径,不能作为红黑树校验标准。满足路径长度条件,结点颜色依然可以违反 4 条规则,后续插入会直接出错。 必须逐条校验四条规则:
- 根结点必须黑色。
- 不能存在连续红色结点(当前结点红色,父结点不能红色)。
- 每一条从根到 nullptr (NIL) 路径,黑色结点数目完全相等。
递归校验代码:
cpp
//内部递归:blackNum 当前路径累计黑色数量;refNum 参考路径黑色计数
bool _Check(Node* root, int blackNum, const int refNum)
{
if(root == nullptr)
{
//走到NIL空结点,一条路径结束
if(blackNum != refNum)
{
cout << "路径黑色结点数目不一致!" << endl;
return false;
}
return true;
}
//规则:不能连续红色,当前结点红,父亲不能红
if(root->_col == RED && root->_parent->_col == RED)
{
cout << "key:" << root->_kv.first << " 存在连续红色结点!" << endl;
return false;
}
//黑色结点计数+1
if(root->_col == BLACK)
{
blackNum++;
}
//递归校验左右子树
return _Check(root->_left, blackNum, refNum)
&& _Check(root->_right, blackNum, refNum);
}
//对外接口
bool IsBalance()
{
if(_root == nullptr)
return true;
//规则:根结点必须黑色
if(_root->_col == RED)
{
cout << "根结点是红色,违反红黑树规则" << endl;
return false;
}
//求参考黑色计数:一直向左走到NIL,统计黑色结点数量作为基准refNum
int refNum = 0;
Node* cur = _root;
while(cur != nullptr)
{
if(cur->_col == BLACK)
refNum++;
cur = cur->_left;
}
return _Check(_root, 0, refNum);
}
6 关于红黑树删除
本章节不实现删除操作 。删除逻辑复杂,分为大量 case,需要处理结点删除后的颜色修复;工程参考《算法导论》、《STL 源码剖析》阅读源码。笔试面试重点考察红黑树规则、插入三种 case、AVL 对比,删除一般不手写。
📔本篇核心考点总结
- 红黑树 4 条核心规则 ①结点红或黑;②根必须黑色;③不能连续红色结点;④任意结点到全部 NIL 空叶子路径黑色结点数量相等。
- 最长路径不超过最短路径 2 倍;时间复杂度\(O(logN)\);对比 AVL,红黑树旋转更少,STL map/set 底层采用红黑树。
- 新增结点默认红色,新增黑色极易破坏黑色计数规则。
- 插入冲突处理三大 case:
- case1:叔叔 uncle 为红色:仅变色,向上迭代,不旋转;
- case2:叔叔为空 / 黑色,LL/RR:单旋 + 变色,结束;
- case3:叔叔为空 / 黑色,LR/RL:双旋 + 变色,结束;
- 循环结束强制根结点置为黑色。
- 红黑树旋转和 AVL 旋转代码一样,没有平衡因子,旋转后只修改结点颜色。
- 校验函数:①根黑色;②禁止连续红结点;③全部路径黑色结点数目相等;不能只靠路径长度倍数校验。
面试简答
Q:红黑树和 AVL 树区别?
A:AVL 依靠平衡因子严格高度平衡;红黑树依靠颜色做近似平衡;AVL 查找略快,插入删除旋转多;红黑树旋转次数少,综合性能更好,STL map set 底层红黑树。
Q:红黑树新结点为什么默认是红色?
A:新增黑色直接破坏规则 4,所有路径黑色计数被打乱,修复成本高;新增红色只会可能触发连续红色,修复逻辑简单。
Q:红黑树插入叔叔结点红色的时候做什么?
A:父亲、叔叔变黑,祖父变红,把祖父当做 cur 继续向上处理,不需要旋转。


