红黑树是一种自平衡的二叉搜索树(BST) ,核心目标是解决普通二叉搜索树(BST)“退化为链表” 的性能问题。它通过「节点颜色规则 + 旋转 / 变色调整」,强制将树的高度控制在 O(logn)(n 为节点数),保证插入、删除、查找操作的时间复杂度稳定为 O(logn)。红黑树是工业级应用最广的平衡树(如 C++ STL 的 set/map、Java TreeMap、Linux 内核的内存管理)。
一、红黑树的核心定位
1. 解决的问题:普通 BST 的缺陷
普通 BST 若按有序序列插入(如 1→2→3→4→5),会退化为单链表,树高变为 n,此时查找 / 插入 / 删除的时间复杂度从 O(logn) 退化为 O(n),完全失去二叉树的优势。
2. 红黑树的优势
- 近似平衡:无需像 AVL 树那样严格控制 “左右子树高度差 ≤1”,仅通过 “黑高一致” 规则控制树高,平衡调整成本更低;
- 高效操作:插入 / 删除最多只需 2 次旋转,远少于 AVL 树的
O(logn)次旋转; - 稳定性:所有操作的时间复杂度稳定为
O(logn),无最坏情况退化。
二、红黑树的基础定义
1. 节点结构(简化版)
红黑树的节点包含 3 个指针(父 / 左 / 右)、1 个颜色标记、1 个值,且引入「NIL 哨兵节点」(空节点)简化边界处理:
template <typename T>
struct RBNode {
typedef RBNode* Pointer;
Pointer parent; // 父节点指针(回溯/旋转必备)
Pointer left; // 左孩子指针
Pointer right; // 右孩子指针
int color; // 颜色:0=黑色(BLACK),1=红色(RED)
T key; // 节点值(用于比较的键)
static Pointer NIL; // 全局 NIL 哨兵节点(所有空节点指向它)
};
// 初始化 NIL 节点(所有空指针指向它,颜色为黑)
template <typename T>
typename RBNode<T>::Pointer RBNode<T>::NIL = new RBNode<T>{nullptr, nullptr, nullptr, 0, T()};
- NIL 节点:所有叶子节点的左 / 右指针、根节点的父指针都指向 NIL,统一边界处理(避免判断
nullptr); - NIL 节点固定为黑色,是红黑树规则的一部分。
2. 红黑树的 5 条核心规则(平衡的根本)
红黑树的所有平衡调整都是为了满足以下 5 条不变式(规则),缺一不可:
| 规则编号 | 规则描述 | 规则的核心作用 |
|---|---|---|
| 1 | 每个节点要么是红色,要么是黑色 | 颜色是平衡调整的 “工具” |
| 2 | 根节点必须是黑色 | 避免根节点染红导致全树黑高不一致 |
| 3 | 所有 NIL 哨兵节点都是黑色 | 统一叶子节点的边界规则 |
| 4 | 若一个节点是红色,则其两个子节点必须是黑色(红节点不能 “相邻”) | 限制红节点的数量,避免连续红节点 |
| 5 | 从任意节点到其所有后代 NIL 节点的路径上,黑色节点的数量完全相同(黑高一致) | 控制树的最大高度(最长路径 ≤ 2× 最短路径) |
关键概念:黑高(Black Height)
- 定义:从某节点到其后代 NIL 节点的路径上,黑色节点的数量(不包含当前节点);
- 规则 5 要求:任意节点的所有路径黑高相同,这是红黑树 “近似平衡” 的核心。
3. 红黑树的高度上限(核心推导)
由 5 条规则可推导出:红黑树的高度 h ≤ 2×log₂(n+1) (n 为非 NIL 节点数),保证了操作的 O(logn) 复杂度:
- 最短路径:全黑节点,长度 = 黑高
bh; - 最长路径:红黑交替,长度 = 2×
bh(规则 4 限制红节点不相邻); - 由规则 5,所有路径黑高相同,因此最长路径 ≤ 2× 最短路径,树高不会退化。
三、红黑树的核心操作(旋转 + 插入 + 删除)
红黑树的操作分为「基础操作(查找 / 旋转)」和「核心操作(插入 / 删除)」,其中旋转是平衡调整的唯一 “物理手段” ,变色是辅助手段。
1. 基础操作 1:查找(与普通 BST 完全一致)
红黑树的查找逻辑和普通 BST 无区别,仅利用 “左小右大” 的 BST 特性,时间复杂度 O(logn):
template <typename T>
typename RBNode<T>::Pointer RBTree<T>::search(T key) {
RBNode<T>::Pointer curr = root;
while (curr != RBNode<T>::NIL) {
if (key == curr->key) return curr; // 找到,返回节点
else if (key < curr->key) curr = curr->left; // 往左找
else curr = curr->right; // 往右找
}
return RBNode<T>::NIL; // 未找到,返回 NIL
}
2. 基础操作 2:旋转(左旋 / 右旋)
旋转是调整树形态的核心操作,不破坏 BST 特性(左小右大),仅改变节点的父子关系。
(1)左旋(Left Rotate)
以目标节点 x 为轴,将其右孩子 y “提上来” 作为新父节点,x 变为 y 的左孩子,y 的原左孩子变为 x 的右孩子:
template <typename T>
void RBTree<T>::leftRotate(RBNode<T>::Pointer x) {
RBNode<T>::Pointer y = x->right; // y 是 x 的右孩子
// 步骤1:y 的左孩子(T2)变为 x 的右孩子
x->right = y->left;
if (y->left != RBNode<T>::NIL) {
y->left->parent = x;
}
// 步骤2:y 的父节点替换为 x 的父节点
y->parent = x->parent;
if (x->parent == RBNode<T>::NIL) { // x 是根节点
root = y;
} else if (x == x->parent->left) { // x 是左孩子
x->parent->left = y;
} else { // x 是右孩子
x->parent->right = y;
}
// 步骤3:x 变为 y 的左孩子
y->left = x;
x->parent = y;
}
(2)右旋(Right Rotate)
与左旋对称,以目标节点 y 为轴,将其左孩子 x “提上来” 作为新父节点:
template <typename T>
void RBTree<T>::rightRotate(RBNode<T>::Pointer y) {
RBNode<T>::Pointer x = y->left; // x 是 y 的左孩子
// 步骤1:x 的右孩子(T2)变为 y 的左孩子
y->left = x->right;
if (x->right != RBNode<T>::NIL) {
x->right->parent = y;
}
// 步骤2:x 的父节点替换为 y 的父节点
x->parent = y->parent;
if (y->parent == RBNode<T>::NIL) { // y 是根节点
root = x;
} else if (y == y->parent->right) { // y 是右孩子
y->parent->right = x;
} else { // y 是左孩子
y->parent->left = x;
}
// 步骤3:y 变为 x 的右孩子
x->right = y;
y->parent = x;
}
旋转的核心特点
- 旋转仅调整指针,时间复杂度
O(1); - 旋转后仍满足 BST 特性(左子树值 < 父节点值 < 右子树值);
- 旋转是平衡调整的 “核心工具”,插入 / 删除的平衡调整都依赖旋转。
3. 核心操作 1:插入(Insert)
插入的核心逻辑:先按 BST 插入,再调整颜色 / 旋转,恢复红黑树规则。
步骤 1:BST 规则插入新节点
- 从根节点开始,找到空的插入位置(NIL 节点);
- 新节点初始颜色设为红色(关键!插入红色节点仅可能违反规则 4,插入黑色节点必然违反规则 5);
- 若插入位置已存在相同值的节点,直接返回(对应
set的 “唯一性”)。
template <typename T>
bool RBTree<T>::insert(T key) {
// 1. 找插入位置(parent 是新节点的父节点)
RBNode<T>::Pointer parent = RBNode<T>::NIL;
RBNode<T>::Pointer curr = root;
while (curr != RBNode<T>::NIL) {
parent = curr;
if (key == curr->key) return false; // 重复值,插入失败(set 特性)
else if (key < curr->key) curr = curr->left;
else curr = curr->right;
}
// 2. 创建新节点(红色)
RBNode<T>::Pointer newNode = new RBNode<T>{
parent, RBNode<T>::NIL, RBNode<T>::NIL, 1, key
};
// 3. 链接新节点到父节点
if (parent == RBNode<T>::NIL) {
root = newNode; // 空树,新节点是根
} else if (key < parent->key) {
parent->left = newNode;
} else {
parent->right = newNode;
}
// 4. 调整平衡,恢复红黑树规则
insertFixup(newNode);
return true;
}
步骤 2:插入后的平衡调整(insertFixup)
插入红色节点后,仅可能违反「规则 4(红节点相邻)」或「规则 2(根节点染红)」,需分 3 种核心场景 调整:
| 场景 | 触发条件 | 调整策略 |
|---|---|---|
| 场景 1 | 叔叔节点是红色 | 父节点 + 叔叔节点 → 黑色;祖父节点 → 红色;将祖父节点作为新节点,继续向上检查 |
| 场景 2 | 叔叔节点是黑色,且新节点是 “右孩子” | 先对父节点左旋,转化为场景 3;再按场景 3 处理 |
| 场景 3 | 叔叔节点是黑色,且新节点是 “左孩子” | 祖父节点 → 红色,父节点 → 黑色;对祖父节点右旋,终止调整 |
补充:若新节点的父节点是祖父节点的右孩子,场景 2/3 对称(左旋↔右旋)。
平衡调整代码(核心逻辑):
template <typename T>
void RBTree<T>::insertFixup(RBNode<T>::Pointer z) {
// 仅当父节点是红色时,才需要调整(否则规则 4 不违反)
while (z->parent->color == 1) {
if (z->parent == z->parent->parent->left) { // 父节点是祖父的左孩子
RBNode<T>::Pointer uncle = z->parent->parent->right; // 叔叔节点
// 场景 1:叔叔是红色 → 变色 + 向上传递
if (uncle->color == 1) {
z->parent->color = 0; // 父节点变黑
uncle->color = 0; // 叔叔节点变黑
z->parent->parent->color = 1; // 祖父节点变红
z = z->parent->parent; // 祖父节点作为新节点继续检查
} else {
// 场景 2:叔叔是黑色,z 是右孩子 → 左旋父节点,转场景 3
if (z == z->parent->right) {
z = z->parent;
leftRotate(z);
}
// 场景 3:叔叔是黑色,z 是左孩子 → 变色 + 右旋祖父节点
z->parent->color = 0;
z->parent->parent->color = 1;
rightRotate(z->parent->parent);
}
} else { // 父节点是祖父的右孩子(对称逻辑)
RBNode<T>::Pointer uncle = z->parent->parent->left;
if (uncle->color == 1) {
z->parent->color = 0;
uncle->color = 0;
z->parent->parent->color = 1;
z = z->parent->parent;
} else {
if (z == z->parent->left) {
z = z->parent;
rightRotate(z);
}
z->parent->color = 0;
z->parent->parent->color = 1;
leftRotate(z->parent->parent);
}
}
}
// 确保根节点是黑色(规则 2)
root->color = 0;
}
插入示例(直观理解)
插入前红黑树:
4 (黑)
/ \
2(红) 6(红)
插入节点 5(红色)→ 父节点 6 是红色,违反规则 4:
- 叔叔节点是 2(红色)→ 场景 1:2、6 变黑色,4 变红色;
- 4 是根节点 → 强制改回黑色;最终平衡后的树:
4 (黑)
/ \
2(黑) 6(黑)
/
5(红)
4. 核心操作 2:删除(Delete)
删除是红黑树最复杂的操作,核心逻辑:先按 BST 删除节点,再修复红黑树规则。
步骤 1:BST 规则删除节点
按待删除节点的子节点数量,分 3 种情况(与普通 BST 一致):
- 叶子节点(无孩子) :直接删除,父节点的对应指针指向 NIL;
- 单孩子节点:将父节点的指针指向该节点的子节点,再删除原节点;
- 双孩子节点:找到「中序后继」(右子树的最左节点),用后继节点的值覆盖当前节点,再删除后继节点(后继节点必是情况 1/2)。
步骤 2:删除后的平衡调整(deleteFixup)
删除节点后,仅当「删除的是黑色节点」时,才会破坏规则 5(黑高不一致),需要修复;若删除的是红色节点,规则无破坏,直接结束。
修复的核心思路:通过 “借黑”“旋转 + 变色”,让所有路径的黑高恢复一致,分 4 种核心场景(对称共 8 种):
| 场景 | 触发条件 | 调整策略 |
|---|---|---|
| 场景 1 | 兄弟节点是红色 | 兄弟节点 → 黑色,父节点 → 红色;对父节点左旋,转化为场景 2/3/4 |
| 场景 2 | 兄弟节点是黑色,且兄弟的两个孩子都是黑色 | 兄弟节点 → 红色;将父节点作为新节点,继续向上检查 |
| 场景 3 | 兄弟节点是黑色,兄弟的左孩子红、右孩子黑 | 兄弟节点 → 红色,左孩子 → 黑色;对兄弟节点右旋,转化为场景 4 |
| 场景 4 | 兄弟节点是黑色,兄弟的右孩子红 | 兄弟节点继承父节点颜色,父节点 → 黑色,兄弟的右孩子 → 黑色;对父节点左旋,终止调整 |
删除的核心特点
- 平衡调整最多需要 2 次旋转;
- 删除后,除被删除节点的迭代器外,其他迭代器均不失效(红黑树仅调整指针,未移动节点内存)。
四、红黑树 vs AVL 树(核心对比)
| 特性 | 红黑树 | AVL 树 |
|---|---|---|
| 平衡标准 | 黑高一致(近似平衡) | 左右子树高度差 ≤1(严格平衡) |
| 树高上限 | 2×log₂(n+1) | 1.44×log₂(n+1) |
| 插入旋转次数 | 最多 2 次 | 最多 O (logn) 次 |
| 删除旋转次数 | 最多 2 次 | 最多 O (logn) 次 |
| 查找效率 | 略低(树高稍高) | 略高(更平衡) |
| 适用场景 | 频繁插入 / 删除(如 set/map) | 频繁查找(如数据库索引) |
五、红黑树的工业级应用
- C++ STL:
set/map/multiset/multimap的底层实现; - Java 集合:
TreeMap/TreeSet的底层实现; - Linux 内核:内存管理(slab 分配器)、进程调度的红黑树;
- 数据库:MySQL 的索引(B+ 树的基础是平衡树思想,红黑树是核心参考);
- 分布式系统:Redis 的有序集合(zset)在元素较多时,底层用红黑树实现。
六、核心总结
- 红黑树是「近似平衡」的 BST,通过 5 条颜色规则控制树高在
O(logn); - 旋转是平衡调整的核心工具,不破坏 BST 特性,时间复杂度
O(1); - 插入:先 BST 插入(红节点),再修复规则 4,最多 2 次旋转;
- 删除:先 BST 删除,再修复规则 5(仅删黑节点时),最多 2 次旋转;
- 红黑树的优势是「插入 / 删除调整成本低」,是工业级平衡树的首选。
理解红黑树的核心是:颜色规则是 “平衡的约束”,旋转 / 变色是 “平衡的手段”,最终目标是维持树高 O (logn),保证操作高效。