一、红黑树是什么?
红黑树(Red-Black Tree) 是一种自平衡的二叉查找树,引入它的核心目的是:解决普通二叉查找树在插入/删除后可能退化成长链表的问题 ,保证查找操作的时间复杂度稳定在 O(log n)。
应用场景(高频面试问)
| 场景 | 如何用红黑树 |
|---|---|
C++ std::map/std::set |
底层基于红黑树实现 |
Java TreeMap |
底层是红黑树 |
epoll 事件监听 |
Linux内核用红黑树管理文件描述符,O(log n) 查找 |
| Nginx 定时器 | 用红黑树管理超时事件,最快找到最近要超时的事件 |
| Linux 内核 CFS 调度器 | 用红黑树管理进程按vruntime排序 |
为什么不用数组/链表?
- 数组二分查找:O(log n) 但插入删除 O(n)
- 链表:插入O(1) 但查找 O(n)
- 红黑树:查找 O(log n),插入/删除也是 O(log n),两者兼得
二、红黑树的5条性质
- 每个节点要么是红色,要么是黑色
- 根节点必须是黑色
- 每个叶子节点(NIL/空节点)都是黑色
- 如果一个节点是红色,则它的两个子节点必须是黑色(不能连续红)
- 从任意节点到其所有子孙节点的任意路径上,黑色节点数量相同
第5条是核心 :它决定了红黑树的平衡性。从根到叶子节点的最短路径 不超过最长路径的2倍。
注意:代码实现中通常用一个哨兵节点(nil) 表示所有叶子节点,避免大量空指针判断。
三、红黑树的数据结构定义
c
#define RED 0
#define BLACK 1
typedef int KEY_TYPE;
// 宏:方便复用左右指针+父指针+颜色
#define RBTREE_ENTRY(name, type) \
struct name { \
struct type *left; \
struct type *right; \
struct type *parent; \
unsigned char color; \
}
// 节点定义
typedef struct _rbtree_node {
KEY_TYPE key;
void *value;
RBTREE_ENTRY(, _rbtree_node) node; // 复用指针结构体
} rbtree_node;
// 红黑树整体结构
typedef struct _rbtree {
rbtree_node *root; // 根节点
rbtree_node *nil; // 哨兵节点,表示空叶子
} rbtree;
为什么要用 nil 哨兵?
- 避免代码中到处判断 NULL
- 所有空叶子都用 T->nil 表示,统一处理
四、红黑树核心操作:旋转
旋转是红黑树保持平衡 的核心手段,分为左旋 和右旋,两者互为逆操作。
旋转核心思想
把一个节点往下拉,它的一边子树"甩上去"接替它的位置
记住一个通用规律:
- 旋转方向 = 子树要"甩上去"的方向
- 左旋 = 右子左上 顶替;右旋 = 左子右上顶替
左旋图解(以x为轴心)
旋转前: 旋转后:
x y
/ \ / \
α y x γ
/ \ / \
β γ α β
步骤分解:
1. x的右指针指向y的左子树β
2. y的父指针指向x的父节点
3. y代替x的位置向上提(x的父节点指向y)
4. y的左指针指向x,x的父指针指向y
记忆口诀:x向右下旋,y向左上顶
通俗理解:x向右下倒,y代替x原来的位置。
右旋图解(以x为轴心)
旋转前: 旋转后:
x y
/ \ / \
y γ α x
/ \ / \
α β β γ
步骤分解:
1. x的左指针指向y的右子树β
2. y的父指针指向x的父节点
3. y代替x的位置向上提(x的父节点指向y)
4. y的右指针指向x,x的父指针指向y
记忆口诀:x向左下旋,y向右上顶
通俗理解:x向左下倒,y代替x原来的位置。
代码实现
c
// 左旋
void rbtree_left_rotate(rbtree *T, rbtree_node *x) {
rbtree_node *y = x->right; // y是x的右子树
// 1. x的右指针指向y的左子树
x->right = y->left;
if (y->left != T->nil) {
y->left->parent = x;
}
// 2. y的父指针指向x的父节点
y->parent = x->parent;
// 3. 更新父节点的左右指针指向
if (x->parent == T->nil) { // x是根节点
T->root = y;
} else if (x == x->parent->left) { // x是左子树
x->parent->left = y;
} else { // x是右子树
x->parent->right = y;
}
// 4. y的左指针指向x,x的父指针指向y
y->left = x;
x->parent = y;
}
// 右旋(与左旋对称,把left/right互换即可)
void rbtree_right_rotate(rbtree *T, rbtree_node *x) {
rbtree_node *y = x->left; // y是x的左子树
x->left = y->right;
if (y->right != T->nil) {
y->right->parent = x;
}
y->parent = x->parent;
if (x->parent == T->nil) {
T->root = y;
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
y->right = x;
x->parent = y;
}
注意:旋转只改变节点之间的父子关系,不破坏BST的有序性(中序遍历顺序不变)。
五、插入操作与修复
插入
c
void rbtree_insert(rbtree *T, rbtree_node *z) {
rbtree_node *x = T->root;
rbtree_node *y = T->nil;
// 1. 找插入位置(标准BST)
while (x != T->nil) {
y = x;
if (z->key < x->key) {
x = x->left;
} else if (z->key > x->key) {
x = x->right;
} else {
// 键已存在,不插入
return;
}
}
// 2. 挂到父节点下
if (y == T->nil) {
T->root = z; // 空树,新节点成为根
} else if (z->key < y->key) {
y->left = z;
} else {
y->right = z;
}
// 3. 初始化新节点
z->parent = y;
z->left = T->nil;
z->right = T->nil;
z->color = RED; // 重要:新节点设为红色,不改变黑高
}
插入后修复(重点!)
核心原则:如果父节点是红色,就需要调整(红红不能相连)。
c
void rbtree_insert_fixup(rbtree *T, rbtree_node *z) {
while (z->parent->color == RED) {
if (z->parent == z->parent->parent->left) {
// 父节点是爷爷的左子树
rbtree_node *y = z->parent->parent->right; // 叔叔节点
if (y->color == RED) {
// 情况1:叔叔是红色 → 变色
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent; // 爷爷变成新的z,继续向上检查
} else {
// 情况2:叔叔是黑色
if (z == z->parent->right) {
// 子树形态是"三角型" → 先左旋变成"直线型"
z = z->parent;
rbtree_left_rotate(T, z);
}
// 情况3:子树形态是"直线型" → 右旋 + 变色
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_right_rotate(T, z->parent->parent);
}
} else {
// 父节点是爷爷的右子树,对称处理
}
}
// 最后确保根节点是黑色
T->root->color = BLACK;
}
修复的3种情况总结
| 情况 | 条件 | 处理 |
|---|---|---|
| 情况1 | 父红 + 叔红 | 父变黑,叔变黑,爷爷变红,z上移到爷爷 |
| 情况2 | 父红 + 叔黑 + z是右子 | z左旋变成情况3(三角→直线) |
| 情况3 | 父红 + 叔黑 + z是左子 | 父变黑,爷爷变红,右旋爷爷 |
旋转次数 :最多 2次(情况2左旋 + 情况3右旋)。
修复过程图解
情况1(变色):
G(红) G(红)
/ \ → / \
P(红) U(红) P(黑) U(黑)
/ /
z(红) z(红)
往上传播一层
情况2(三角型)→ 情况3:
G G
/ /
P(红) → z(红)
\ /
z(红) P(红)
(先左旋P,z变父)
情况3(直线型)→ 完成:
G(红) P(黑)
/ / \
P(红) → α G(红)
/ \
α β
(右旋G,PG变色)
旋转次数:最多2次
六、删除操作
删除比插入更复杂,因为删黑色节点会破坏黑高平衡。分3种情况:
| 情况 | 条件 | 处理 |
|---|---|---|
| 情况A | 删的是红色叶子 | 直接删,不破坏黑高 |
| 情况B | 删黑色但有红色子节点 | 用红色子节点补位,变黑 |
| 情况C | 删黑色叶子(双子空) | 借节点 + 递归修复 |
删除修复用 "双重黑色" 概念理解:删了一个黑节点,某条路径少了一个黑,需要从别处借或向上传播。
(如果面试不考删除,先掌握插入;必问的话再深入)
七、红黑树 vs 其他数据结构
| 数据结构 | 查找 | 插入 | 删除 | 特点 |
|---|---|---|---|---|
| 红黑树 | O(log n) | O(log n) | O(log n) | 平衡工程实现,Linux/内核常用 |
| AVL树 | O(log n) | O(log n) | O(log n) | 更严格平衡,查询更快 |
| 跳表 | O(log n) | O(log n) | O(log n) | 实现简单,Redis ZSET底层 |
| B/B+树 | O(log n) | O(log n) | O(log n) | 多叉树,适合磁盘,数据库索引 |
| Hash | O(1) | O(1) | O(1) | 最快,但无序,不适合范围查询 |
八、面试高频提问
Q1:红黑树 vs AVL树区别?
- AVL更严格(左右子树高度差≤1),查询更快但插入删除更慢
- 红黑树更宽松,插入删除旋转次数更少,实际工程用得更多(Linux内核、epoll等)
- 工程实现用红黑树,面试可能两种都问
Q2:为什么红黑树插入新节点用红色?
- 红色不改变黑高,只需要修复"红红相连"的问题
- 黑色会改变黑高,影响范围更广,修复更麻烦
Q3:红黑树能保证O(log n)吗?
- 能。因为5条性质保证了树高 ≤ 2log₂(n+1),所以查找/插入/删除都是O(log n)
Q4:旋转为什么不破坏BST性质?
- 旋转只改变节点之间的父子关系,不改变节点值的大小顺序
- 中序遍历结果旋转前后完全一样
根据零声教育教学写作https://github.com/0voice