【数据结构】红黑树详解

一、红黑树是什么?

红黑树(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条性质

  1. 每个节点要么是红色,要么是黑色
  2. 根节点必须是黑色
  3. 每个叶子节点(NIL/空节点)都是黑色
  4. 如果一个节点是红色,则它的两个子节点必须是黑色(不能连续红)
  5. 从任意节点到其所有子孙节点的任意路径上,黑色节点数量相同

第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

相关推荐
,,?!,8 小时前
数据结构算法-排序算法
数据结构·算法·排序算法
‎ദ്ദിᵔ.˛.ᵔ₎9 小时前
C++哈希表
数据结构·c++·散列表
阿旭超级学得完10 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法
云淡风轻~窗明几净10 小时前
关于角谷猜想的五行小猜想
数据结构·算法
Languorous.10 小时前
C++数据结构进阶|并查集(Union-Find)详解:从原理到面试实战
数据结构·c++·面试
Languorous.11 小时前
C++数据结构进阶|堆(Heap)详解:从手写实现到面试高频实战
数据结构·c++·面试
玛卡巴卡ldf12 小时前
【LeetCode 手撕算法】(栈)有效括号、最小栈、字符串解码、每日温度、柱状图最大矩形
java·数据结构·算法·leetcode·力扣
我头发还没掉光~12 小时前
P4147 玉蟾宫
数据结构·c++·算法
枕星而眠13 小时前
栈(Stack)与队列(Queue)核心知识总结
c语言·数据结构·后端·链表
Little At Air13 小时前
LinuxOS阻塞队列模型(单生产者单消费者)
linux·数据结构·c++