建议先阅读: 树 BST AVL


原理

红黑树(Red-Black Tree)是一种自平衡二叉查找树,每个节点额外存储一个颜色位(红色或黑色),通过颜色约束保证树近似平衡。它是 C++ 标准库中 setmapmultisetmultimap 的底层实现。

红黑树在哪里

C++ 的 std::map 和 Java 的 TreeMap 底层都是红黑树——插入/删除后迭代器不失效,中序遍历即有序。Linux 内核的 CFS 调度器用红黑树按 vruntime 排序就绪进程,每次调度取最左节点。epoll 用红黑树存储被监视的文件描述符,O(log n) 判断是否已在监听集合。nginx 用红黑树管理定时器,按超时时间排序。这些场景的共同特征——插入/删除频繁且需要有序——正是红黑树的设计目标:比 AVL 旋转更少,比普通 BST 保证平衡。

五个性质

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 每个叶子(NIL 空节点)是黑色
  4. 红色节点的两个子节点必须是黑色(不能有连续的红色)
  5. 从任意节点到其每个叶子的所有路径包含相同数目的黑色节点
graph TD
 subgraph 红黑树示例(B=黑, R=红)
 N11["11 B"] --> N21["2 R"]
 N11 --> N31["14 B"]
 N21 --> N41["1 B"]
 N21 --> N51["7 B"]
 N31 --> N61["15 R"]
 N31 --> N71["NIL"]
 N41 --> N81["NIL"]
 N41 --> N91["NIL"]
 N51 --> N101["5 R"]
 N51 --> N111["8 R"]
 N61 --> N121["NIL"]
 N61 --> N131["NIL"]
 end
 style N11 fill:#333,color:#fff
 style N31 fill:#333,color:#fff
 style N41 fill:#333,color:#fff
 style N51 fill:#333,color:#fff
 style N21 fill:#f00,color:#fff
 style N61 fill:#f00,color:#fff
 style N101 fill:#f00,color:#fff
 style N111 fill:#f00,color:#fff
 style N71 fill:#999,color:#fff
 style N81 fill:#999,color:#fff
 style N91 fill:#999,color:#fff
 style N121 fill:#999,color:#fff
 style N131 fill:#999,color:#fff

性质验证自测

选择题常见题型:给一棵树,判断是否满足五个性质。逐条过:

自测 1:以下树是否为合法红黑树?

graph TD
    N10["10(B)"] --> N5["5(R)"]
    N10 --> N15["15(R)"]
    N5 --> N3["3(B)"]
    N5 --> N8["8(B)"]
    N15 --> N20["20(R)"]
    style N10 fill:#333,color:#fff
    style N5 fill:#f00,color:#fff
    style N15 fill:#f00,color:#fff
    style N3 fill:#333,color:#fff
    style N8 fill:#333,color:#fff
    style N20 fill:#f00,color:#fff

答案:不合法——违反性质 5(黑高不等)。路径 10→5→3→NIL 有 3 个黑节点(10,3,NIL),而路径 10→15→NIL 只有 2 个黑节点(10,NIL)。若要合法,需在 15 的左子补一个黑色节点使右路径也有 3 黑。关键教训:验证性质 5 必须逐条路径数黑色节点,不能只看局部。

自测 2:以下树是否为合法红黑树?

graph TD
    N8["8(B)"] --> N4["4(R)"]
    N8 --> N12["12(R)"]
    N4 --> N2["2(B)"]
    N4 --> N6["6(B)"]
    N12 --> N10["10(B)"]
    N12 --> N14["14(B)"]
    style N8 fill:#333,color:#fff
    style N4 fill:#f00,color:#fff
    style N12 fill:#f00,color:#fff
    style N2 fill:#333,color:#fff
    style N6 fill:#333,color:#fff
    style N10 fill:#333,color:#fff
    style N14 fill:#333,color:#fff

答案:合法。① 根黑 ② 红色 4、12 的子节点全黑 ③ 每条到 NIL 的路径有 3 个黑色节点。全部五条性质满足。

自测 3:以下树违反了哪条性质?

graph TD
    N10["10(B)"] --> N5["5(R)"]
    N10 --> N15["15(B)"]
    N5 --> N3["3(R)"]
    N5 --> N8["8(R)"]
    style N10 fill:#333,color:#fff
    style N5 fill:#f00,color:#fff
    style N15 fill:#333,color:#fff
    style N3 fill:#f00,color:#fff
    style N8 fill:#f00,color:#fff

答案:违反性质 4——节点 5 是红色,其左子 3 也是红色,构成红-红相邻。若 15(叔叔)是黑色,则需旋转+变色修复。

复杂度

操作平均最坏说明
查找O(log n)O(log n)高度不超过 2*log(n+1)
插入O(log n)O(log n)BST 插入 + 最多 2 次旋转
删除O(log n)O(log n)BST 删除 + 最多 3 次旋转
空间O(n)O(n)每个节点额外 1 bit

红黑树高度上界证明

定理:一棵有 个内部节点的红黑树,其高度满足:

证明

  1. 将红黑树中所有红色节点”合并”到其父节点(黑色),得到一棵 2-3-4 树
  2. 合并后所有叶子在同一层,每个节点有 2~4 个子节点
  3. 合并后的树高度为 (黑色高度),满足:
  1. 由于红色节点不能连续,有
  2. 代入得 ,两边取对数:

推论:红黑树的查找、插入、删除时间复杂度均为

红黑树 vs AVL 树

特性红黑树AVL 树
平衡标准近似平衡(任意路径上黑色节点数相等)严格平衡(高度差 1)
树高上限
查找稍慢(树稍高)更快(树更矮)
插入旋转最多 2 次可能 次(向根传播)
删除旋转最多 3 次可能
适用场景插入/删除频繁(std::map, epoll查找频繁(数据库索引的极少更新场景)

红黑树与 2-3-4 树的等价性

红黑树是 2-3-4 树(B 树家族中 m=4 的特例)的二叉树表示。将所有红色节点”推入”其黑色父节点中,红黑树即转化为 2-3-4 树:

graph TD
 subgraph "红黑树"
 B1["10 (B)"] --> R1["5 (R)"]
 B1 --> R2["15 (R)"]
 R1 --> B2["2 (B)"]
 R1 --> B3["7 (B)"]
 end
 subgraph "等价 2-3-4 树 — 红色节点被'吸收'到黑色父节点"
 NODE["[5|10|15] (3-node + red children)<br/>子节点: 2, 7"]
 end

这个同构揭示了红黑树设计的本质:红链接 = 属于同一个 2-3-4 节点的键。性质 4(不能有连续红色)保证了两个红链接不能相邻——因为如果两个红色节点互为父子,它们在 2-3-4 树中会产生超过 3 个键的超大节点。

理解这个等价性后,红黑树的旋转与颜色变换就不再是死记规则——每次操作对应 2-3-4 树中节点键数的增减(从 4-node 分裂为两个 2-node,或相邻 2-node 合并为 3-node)。

插入修复:三种情况

新节点总是红色(默认)。插入后可能违反性质 4(红-红相邻)。修复按新节点的叔叔节点颜色分三种情况:

flowchart TD
 INSERT["插入红色节点 Z<br/>父节点 P 也是红色"] --> CHECK{"叔叔(U)颜色?"}
 CHECK -->|"U 是红色"| CASE1["情况1: 颜色翻转<br/>P和U变黑, G变红<br/>问题上移到G"]
 CASE1 --> UP["对 G 递归修复"]
 CHECK -->|"U 是黑色<br/>Z/P/G 构成折线"| CASE2["情况2: 旋转拉直<br/>对 P 旋转使 Z/P/G 成直线"]
 CASE2 --> CASE3
 CHECK -->|"U 是黑色<br/>Z/P/G 成直线"| CASE3["情况3: 旋转+变色<br/>旋转 G, 交换 P 和 G 的颜色<br/>修复完成"]
  • 情况 1(U 红色):父叔双双从红变黑,爷从黑变红。这保持了性质 5(黑高不变),但可能在更高层引入红-红冲突——递归向上修复
  • 情况 2(U 黑色,Z-P-G 折线):旋转 P 使 Z-P-G 拉直为一线,转化为情况 3
  • 情况 3(U 黑色,Z-P-G 直线):旋转 G 使其下降,P 上升为新的子树根。交换 P 和 G 的颜色后,修复完成——不会向上传播

插入修复至多执行 次情况 1(颜色翻转-上移),最后以一次旋转(情况 2+3 组合)收尾。

红黑树在工程中的应用

  • C++ std::map / std::set:libstdc++ 和 libc++ 均用红黑树实现。原因:迭代器在插入/删除后不失效(与 vector 不同),中序遍历即有序,且插入/删除最多 3 次旋转
  • Linux 内核 CFS 调度器:就绪队列用红黑树按 vruntime 排序。每次时钟中断后更新当前进程的 vruntime 并重新入树——插入/删除极频繁,红黑树的低旋转次数是首选
  • Linux epoll:用红黑树存储被监视的 fd。查找 fd 是否已在监听集合中——O(log n)
  • Java TreeMap / TreeSet:红黑树实现的有序映射和集合
  • nginx 定时器:用红黑树存储超时事件,按超时时间排序——每次取出最小超时时间的事件

详见 操作系统 — CFS 调度器

红黑树插入(含修复)

#include <stdlib.h>
 
typedef enum { RED, BLACK } Color;
 
typedef struct RBNode {
 int data;
 Color color;
 struct RBNode *left, *right, *parent;
} RBNode;
 
typedef struct {
 RBNode* root;
 RBNode* NIL; // 哨兵空节点(黑色)
} RBTree;
 
RBNode* rb_create_node(int val) {
 RBNode* node = malloc(sizeof(RBNode));
 node->data = val;
 node->color = RED;
 node->left = node->right = node->parent = NULL;
 return node;
}
 
void rb_init(RBTree* t) {
 t->NIL = malloc(sizeof(RBNode));
 t->NIL->color = BLACK;
 t->NIL->left = t->NIL->right = t->NIL->parent = NULL;
 t->root = t->NIL;
}
 
static void rb_left_rotate(RBTree* t, RBNode* x) {
 RBNode* y = x->right;
 x->right = y->left;
 if (y->left != t->NIL) y->left->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->left = x;
 x->parent = y;
}
 
static void rb_right_rotate(RBTree* t, RBNode* y) {
 RBNode* x = y->left;
 y->left = x->right;
 if (x->right != t->NIL) x->right->parent = y;
 x->parent = y->parent;
 if (y->parent == t->NIL) t->root = x;
 else if (y == y->parent->left) y->parent->left = x;
 else y->parent->right = x;
 x->right = y;
 y->parent = x;
}
 
static void rb_insert_fixup(RBTree* t, RBNode* z) {
 while (z->parent != t->NIL && z->parent->color == RED) {
 if (z->parent == z->parent->parent->left) {
 RBNode* y = z->parent->parent->right;
 if (y->color == RED) {
 z->parent->color = BLACK;
 y->color = BLACK;
 z->parent->parent->color = RED;
 z = z->parent->parent;
 } else {
 if (z == z->parent->right) {
 z = z->parent;
 rb_left_rotate(t, z);
 }
 z->parent->color = BLACK;
 z->parent->parent->color = RED;
 rb_right_rotate(t, z->parent->parent);
 }
 } else {
 RBNode* y = z->parent->parent->left;
 if (y->color == RED) {
 z->parent->color = BLACK;
 y->color = BLACK;
 z->parent->parent->color = RED;
 z = z->parent->parent;
 } else {
 if (z == z->parent->left) {
 z = z->parent;
 rb_right_rotate(t, z);
 }
 z->parent->color = BLACK;
 z->parent->parent->color = RED;
 rb_left_rotate(t, z->parent->parent);
 }
 }
 }
 t->root->color = BLACK;
}
 
int rb_insert(RBTree* t, int value) {
 RBNode* z = rb_create_node(value);
 z->left = z->right = t->NIL;
 
 RBNode* y = t->NIL;
 RBNode* x = t->root;
 while (x != t->NIL) {
 y = x;
 if (z->data < x->data) x = x->left;
 else if (z->data > x->data) x = x->right;
 else { free(z); return 0; } // 不允许重复
 }
 z->parent = y;
 if (y == t->NIL) t->root = z;
 else if (z->data < y->data) y->left = z;
 else y->right = z;
 
 rb_insert_fixup(t, z);
 return 1;
}
 
int rb_search(RBTree* t, int value) {
 RBNode* cur = t->root;
 while (cur != t->NIL) {
 if (value == cur->data) return 1;
 cur = (value < cur->data) ? cur->left : cur->right;
 }
 return 0;
}
 
static void rb_destroy_rec(RBTree* t, RBNode* node) {
 if (node == t->NIL) return;
 rb_destroy_rec(t, node->left);
 rb_destroy_rec(t, node->right);
 free(node);
}
 
void rb_destroy(RBTree* t) {
 rb_destroy_rec(t, t->root);
 free(t->NIL);
 t->root = t->NIL = NULL;
}
 
void rb_inorder(RBTree* t, RBNode* node) {
 if (node == t->NIL) return;
 rb_inorder(t, node->left);
 printf("%d ", node->data);
 rb_inorder(t, node->right);
}
 
void rb_preorder(RBTree* t, RBNode* node) {
 if (node == t->NIL) return;
 printf("%d ", node->data);
 rb_preorder(t, node->left);
 rb_preorder(t, node->right);
}

插入修复的三种情况详解

假设新插入节点为 z(红色),其父节点 p 为红色(违反性质 4),叔叔节点为 u。

情况 1:叔叔 u 是红色

操作:p 和 u 变黑,祖父 g 变红,z 上移到 g 继续修复

原理:父叔同时变黑不会改变黑色高度,但祖父变红可能向上传播冲突

graph TD
 subgraph 情况1:叔叔是红色
 direction TB
 G1["g(黑)"] --> P1["p(红) "]
 G1 --> U1["u(红)"]
 P1 --> Z1["z(红) NEW"]
 P1 --> NIL1["NIL"]
 U1 --> NIL2["NIL"]
 U1 --> NIL3["NIL"]
 style G1 fill:#333,color:#fff
 style P1 fill:#f00,color:#fff
 style U1 fill:#f00,color:#fff
 style Z1 fill:#f00,color:#fff
 style NIL1 fill:#999,color:#fff
 style NIL2 fill:#999,color:#fff
 style NIL3 fill:#999,color:#fff
 end
 subgraph 变色后
 direction TB
 G2["g(红)"] --> P2["p(黑) "]
 G2 --> U2["u(黑) "]
 P2 --> Z2["z(红)"]
 P2 --> NIL4["NIL"]
 U2 --> NIL5["NIL"]
 U2 --> NIL6["NIL"]
 style G2 fill:#f00,color:#fff
 style P2 fill:#333,color:#fff
 style U2 fill:#333,color:#fff
 style Z2 fill:#f00,color:#fff
 style NIL4 fill:#999,color:#fff
 style NIL5 fill:#999,color:#fff
 style NIL6 fill:#999,color:#fff
 end
 G1 -->|"p,u 变黑 → g 变红 → z=g"| G2

情况 2:叔叔 u 是黑色,z 与 p 同侧(直线型)

操作:以 g 为支点旋转 + 变色(p 变黑,g 变红)

原理:旋转降低树高,变色恢复黑色高度

graph TD
 subgraph 情况2(左左型):叔叔黑色+直线
 direction TB
 G1["g(黑)"] --> P1["p(红) "]
 G1 --> U1["u(黑)"]
 P1 --> Z1["z(红) NEW"]
 P1 --> NIL1["NIL"]
 Z1 --> NIL2["NIL"]
 Z1 --> NIL3["NIL"]
 U1 --> NIL4["NIL"]
 U1 --> NIL5["NIL"]
 style G1 fill:#f00,color:#fff
 style P1 fill:#f00,color:#fff
 style U1 fill:#333,color:#fff
 style Z1 fill:#f00,color:#fff
 style NIL1 fill:#999,color:#fff
 style NIL2 fill:#999,color:#fff
 style NIL3 fill:#999,color:#fff
 style NIL4 fill:#999,color:#fff
 style NIL5 fill:#999,color:#fff
 end
 subgraph 右旋+变色后
 direction TB
 P2["p(黑) "] --> Z2["z(红)"]
 P2 --> G2["g(红)"]
 Z2 --> NIL6["NIL"]
 Z2 --> NIL7["NIL"]
 G2 --> NIL8["NIL"]
 G2 --> U2["u(黑)"]
 style P2 fill:#333,color:#fff
 style Z2 fill:#f00,color:#fff
 style G2 fill:#f00,color:#fff
 style U2 fill:#333,color:#fff
 style NIL6 fill:#999,color:#fff
 style NIL7 fill:#999,color:#fff
 style NIL8 fill:#999,color:#fff
 end
 G1 -->|"右旋 g + p变黑,g变红"| P2

如果 z 是 p 的右孩子(右左型),则先左旋 p 转为左左型 → 按左左型处理。

情况 3:叔叔 u 是黑色,z 与 p 异侧(三角型)

操作:先用一次旋转转为直线型,再按情况 2 处理

原理:三角型无法通过单次旋转恢复,需两次旋转

graph TD
 subgraph 情况3(左右型):叔叔黑色+三角
 direction TB
 G1["g(黑)"] --> P1["p(红) "]
 G1 --> U1["u(黑)"]
 P1 --> NIL1["NIL"]
 P1 --> Z1["z(红) NEW"]
 Z1 --> NIL2["NIL"]
 Z1 --> NIL3["NIL"]
 U1 --> NIL4["NIL"]
 U1 --> NIL5["NIL"]
 style G1 fill:#f00,color:#fff
 style P1 fill:#f00,color:#fff
 style U1 fill:#333,color:#fff
 style Z1 fill:#f00,color:#fff
 style NIL1 fill:#999,color:#fff
 style NIL2 fill:#999,color:#fff
 style NIL3 fill:#999,color:#fff
 style NIL4 fill:#999,color:#fff
 style NIL5 fill:#999,color:#fff
 end
 subgraph 左旋p后(转为左左型)
 direction TB
 G2["g(黑)"] --> Z2["z(红)"]
 G2 --> U2["u(黑)"]
 Z2 --> P2["p(红)"]
 Z2 --> NIL6["NIL"]
 P2 --> NIL7["NIL"]
 P2 --> NIL8["NIL"]
 U2 --> NIL9["NIL"]
 U2 --> NIL10["NIL"]
 style G2 fill:#f00,color:#fff
 style Z2 fill:#f00,color:#fff
 style U2 fill:#333,color:#fff
 style P2 fill:#f00,color:#fff
 style NIL6 fill:#999,color:#fff
 style NIL7 fill:#999,color:#fff
 style NIL8 fill:#999,color:#fff
 style NIL9 fill:#999,color:#fff
 style NIL10 fill:#999,color:#fff
 end
 G1 -->|"左旋 p"| G2
 G2 -.->|"再按情况2 右旋 g + 变色"| END[" 平衡"]

红黑树删除(含修复)

RBNode* rb_minimum(RBTree* t, RBNode* node) {
 while (node->left != t->NIL)
  node = node->left;
 return node;
}
 
static void rb_transplant(RBTree* t, RBNode* u, RBNode* v) {
 if (u->parent == t->NIL)
  t->root = v;
 else if (u == u->parent->left)
  u->parent->left = v;
 else
  u->parent->right = v;
 v->parent = u->parent;
}
 
static void rb_delete_fixup(RBTree* t, RBNode* x) {
 while (x != t->root && x->color == BLACK) {
  if (x == x->parent->left) {
   RBNode* w = x->parent->right; // 兄弟节点
   if (w->color == RED) {
    // 情况 1:兄弟是红色 → 变色 + 旋转,转化为情况 2/3/4
    w->color = BLACK;
    x->parent->color = RED;
    rb_left_rotate(t, x->parent);
    w = x->parent->right;
   }
   if (w->left->color == BLACK && w->right->color == BLACK) {
    // 情况 2:兄弟是黑色且两个子节点都是黑色 → 兄弟变红,上移
    w->color = RED;
    x = x->parent;
   } else {
    if (w->right->color == BLACK) {
     // 情况 3:兄弟是黑色,右子黑左子红 → 变色 + 旋转,转化为情况 4
     w->left->color = BLACK;
     w->color = RED;
     rb_right_rotate(t, w);
     w = x->parent->right;
    }
    // 情况 4:兄弟是黑色,右子红色 → 旋转 + 变色,修复完成
    w->color = x->parent->color;
    x->parent->color = BLACK;
    w->right->color = BLACK;
    rb_left_rotate(t, x->parent);
    x = t->root;
   }
  } else {
   // 对称情况:x 是右孩子
   RBNode* w = x->parent->left;
   if (w->color == RED) {
    w->color = BLACK;
    x->parent->color = RED;
    rb_right_rotate(t, x->parent);
    w = x->parent->left;
   }
   if (w->right->color == BLACK && w->left->color == BLACK) {
    w->color = RED;
    x = x->parent;
   } else {
    if (w->left->color == BLACK) {
     w->right->color = BLACK;
     w->color = RED;
     rb_left_rotate(t, w);
     w = x->parent->left;
    }
    w->color = x->parent->color;
    x->parent->color = BLACK;
    w->left->color = BLACK;
    rb_right_rotate(t, x->parent);
    x = t->root;
   }
  }
 }
 x->color = BLACK;
}
 
int rb_delete(RBTree* t, int value) {
 // 查找待删除节点
 RBNode* z = t->root;
 while (z != t->NIL) {
  if (value == z->data) break;
  z = (value < z->data) ? z->left : z->right;
 }
 if (z == t->NIL) return 0; // 未找到
 
 RBNode* y = z;           // y 是实际可能被删除的节点(最多一个子节点)
 Color y_original = y->color;
 RBNode* x;               // x 是 y 的子节点(将替代 y 的位置)
 
 if (z->left == t->NIL) {
  x = z->right;
  rb_transplant(t, z, z->right);
 } else if (z->right == t->NIL) {
  x = z->left;
  rb_transplant(t, z, z->left);
 } else {
  // z 有两个孩子:找后继(右子树最小值)
  y = rb_minimum(t, z->right);
  y_original = y->color;
  x = y->right;
  if (y->parent == z) {
   x->parent = y; // x 可能是 NIL
  } else {
   rb_transplant(t, y, y->right);
   y->right = z->right;
   y->right->parent = y;
  }
  rb_transplant(t, z, y);
  y->left = z->left;
  y->left->parent = y;
  y->color = z->color;
 }
 
 free(z);
 if (y_original == BLACK)
  rb_delete_fixup(t, x);
 return 1;
}

各语言标准库对比

红黑树在工程中通常不直接暴露,而是作为有序集合/映射的底层实现:

语言有序集合(红黑树)有序映射(红黑树)
C无(手写)无(手写)
C++set / multisetmap / multimap
JavaTreeSetTreeMap
Python无(bisect + list 模拟)无(可用 sortedcontainers)
RustBTreeSetBTreeMap

应用场景

  • 有序字典/集合: 需要按键排序 + 范围查询的场景(如按时间查询日志)
  • 区间调度: 用 map 管理会议室预约,lower_bound 快速判断冲突
  • Linux 内核 CFS 调度器: 红黑树管理进程按 vruntime 排序

练习

题号题目说明
220存在重复元素 III平衡树范围查询
981基于时间的键值存储TreeMap 操作
493翻转对有序集合 + 范围计数

核心推演清单

红黑树重点考查判断与记忆,不要求手写代码:

自测位置要点
性质验证 ×3 问五个性质节给树判断是否合法(五条逐一验证)

动手实验

编号题目说明
E1红黑树 vs BST 高度对比顺序插入 1 到 1000 到普通 BST 和红黑树,分别记录每插入 100 个元素后的树高,画出树高增长曲线
E2旋转次数统计随机插入 1000 个元素到红黑树,统计 LL/RR/LR/RL 四种旋转各发生多少次,并与理论比例对比