ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深入理解C++ 容器 之 红黑树

深入理解C++ 容器 之 红黑树

`std::map` 的底层是**红黑树(Red-Black Tree)**,一种自平衡的二叉搜索树。理解红黑树,就理解了 `std::map` 为什么能同时提供**有序遍历**和**O(log n)** 的查找、插入、删除。下面从“为什么选它”讲到“它怎么工作”,最后给出可运行的示例。

---

### 🎯 为什么 `std::map` 选红黑树,而不是 AVL 树?

红黑树和 AVL 树都是自平衡二叉搜索树,但设计取舍不同:

| 维度 | **AVL 树** | **红黑树** |
| :--- | :--- | :--- |
| **平衡严格程度** | 高度平衡(左右子树高度差 ≤ 1) | 近似平衡(最长路径 ≤ 最短路径的 2 倍) |
| **查找性能** | 略优(树更矮) | 略逊,但同属 O(log n) |
| **插入/删除的旋转次数** | 较多,可能 O(log n) 次旋转 | **最多 2 次旋转**(插入)、**最多 3 次旋转**(删除) |
| **重新着色开销** | 无颜色概念 | 有颜色翻转,但旋转少 |
| **适用场景** | 查找远多于修改 | **读写混合**,修改频繁 |

`std::map` 是通用容器,插入和删除是常态操作。红黑树**用少量额外的高度换取更少的旋转**,在读写混合场景下综合性能更好,这是标准库选择它的核心原因。C++ 标准只规定了 `std::map` 的**复杂度要求**(O(log n)),并未强制红黑树,但主流实现(libstdc++、libc++、MSVC STL)都采用红黑树。

---

### 📐 红黑树的五条性质

红黑树在每个节点上增加一个**颜色位**(红或黑),通过以下五条性质维持近似平衡:

1. **每个节点是红色或黑色**。
2. **根节点是黑色**。
3. **所有叶子节点(NIL 哨兵)是黑色**。
4. **红色节点的两个子节点必须是黑色**(即不能有连续的红色节点)。
5. **从任一节点到其所有后代 NIL 叶子节点的路径上,黑色节点数量相同**(称为该节点的“黑高”,black-height)。

> 性质 4 和 5 共同保证:**从根到最远叶子的路径长度不超过到最近叶子路径长度的 2 倍**。这就是“近似平衡”的数学来源——树高被限制在 `2 * log2(n+1)` 以内。

---

### 🌳 直观理解:红黑树与 2-3-4 树的等价

理解红黑树修复操作最有效的方式,是把它看作**2-3-4 树(4 阶 B 树)的二叉表示**:

- **黑色节点 + 红色子节点** 合并后,对应 2-3-4 树中的一个**多键节点**(2-node、3-node 或 4-node)。
- **红色节点**表示它和父节点“在同一个多键节点里”。
- **黑高一致**对应 2-3-4 树中**所有叶子在同一层**。
- **插入修复中的“重新着色”**对应 2-3-4 树节点的**分裂**;**旋转**对应多键节点内部键的**调整**。

这个视角让红黑树的三种插入情况和四种删除情况不再是死记硬背的规则,而是 B 树分裂/合并的自然映射。

---

### 🧱 节点结构:libstdc++ 的实现细节

libstdc++(GCC 的 C++ 标准库)中 `std::map` 的红黑树节点基类大致如下:

```cpp
struct _Rb_tree_node_base {
typedef _Rb_tree_node_base* _Base_ptr;
typedef _Rb_tree_color _Color_type;

_Color_type _M_color; // 红或黑
_Base_ptr _M_parent; // 父指针
_Base_ptr _M_left; // 左孩子
_Base_ptr _M_right; // 右孩子
};
```

`std::map` 的节点在此基础上增加键值对存储。一个关键设计是 **header 节点**:树根的父亲指向一个特殊的 header,header 的左指针指向最左节点(`begin()`),右指针指向最右节点(`end()-1`),这样 `begin()` 和 `--end()` 都是 O(1)。`end()` 迭代器则直接指向 header 本身。

> 注意:C++ 标准没有规定 `std::map` 必须用红黑树,也没有暴露颜色信息。上述结构是 libstdc++ 的实现细节,libc++ 和 MSVC STL 的结构类似但字段命名不同。**你的代码不应依赖这些内部细节**。

---

### 🔄 旋转:红黑树维持平衡的基本动作

旋转是**局部指针重排**,不改变中序遍历顺序,只调整树形。左旋和右旋互为逆操作:

```cpp
// 左旋:x 的右孩子 y 上升,x 下降为 y 的左孩子
void leftRotate(Node* x) {
Node* y = x->right;
x->right = y->left; // y 的左子树挂到 x 的右边
if (y->left != NIL) y->left->parent = x;
y->parent = x->parent;
if (x->parent == NIL) root = y; // x 原来是根
else if (x == x->parent->left) x->parent->left = y;
else x->parent->right = y;
y->left = x;
x->parent = y;
}
```

右旋完全对称。一次旋转是 O(1),红黑树的插入和删除**最多做常数次旋转**,这是它优于 AVL 的关键。

---

### ➕ 插入:BST 插入 + 修复

插入分两步:先按普通二叉搜索树规则插入新节点(**新节点染红色**),然后调用 `insertFixup` 修复被破坏的性质。

新节点染红色是因为:染黑会改变某条路径的黑高,必然破坏性质 5;染红只可能违反性质 4(红红相邻),修复范围更局部。

修复分三种情况(以父节点是祖父的左孩子为例,右孩子镜像对称):

| 情况 | 条件 | 处理 |
| :--- | :--- | :--- |
| **情况 1** | 叔叔节点是**红色** | 父、叔染黑,祖父染红,`z` 上移到祖父,继续检查 |
| **情况 2** | 叔叔是黑色,`z` 是父的**右孩子** | 对父节点左旋,转化为情况 3 |
| **情况 3** | 叔叔是黑色,`z` 是父的**左孩子** | 父染黑,祖父染红,对祖父右旋,结束 |

核心代码(完整可运行版本见后文示例):

```cpp
void insertFixup(Node* z) {
while (z->parent->color == RED) {
if (z->parent == z->parent->parent->left) {
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;
} else {
if (z == z->parent->right) { // 情况 2
z = z->parent;
leftRotate(z);
}
z->parent->color = BLACK; // 情况 3
z->parent->parent->color = RED;
rightRotate(z->parent->parent);
}
} else {
// 镜像:父是祖父的右孩子
}
}
root->color = BLACK; // 性质 2:根始终为黑
}
```

三种情况覆盖了所有可能,循环最多沿树上升 O(log n) 次,每次 O(1),总插入复杂度 **O(log n)**。

---

### ➖ 删除:更复杂的四种情况

删除比插入复杂,因为删除黑色节点会破坏黑高(性质 5)。标准做法是:

1. 按 BST 规则删除目标节点(用后继替换),记录被删除节点的颜色。
2. 如果删除的是**红色**节点,性质未被破坏,结束。
3. 如果删除的是**黑色**节点,用“**双重黑**”概念标记替代节点,调用 `deleteFixup` 修复。

修复分四种情况(以 `x` 是父的左孩子为例):

| 情况 | 条件 | 处理 |
| :--- | :--- | :--- |
| **情况 1** | 兄弟 `w` 是**红色** | 兄弟染黑,父染红,对父左旋,转为情况 2/3/4 |
| **情况 2** | 兄弟黑,兄弟两孩子都黑 | 兄弟染红,`x` 上移到父,继续 |
| **情况 3** | 兄弟黑,兄弟左孩子红、右孩子黑 | 兄弟左孩子染黑,兄弟染红,对兄弟右旋,转为情况 4 |
| **情况 4** | 兄弟黑,兄弟右孩子红 | 兄弟取父色,父染黑,兄弟右孩子染黑,对父左旋,结束 |

删除修复最多 **3 次旋转**,总复杂度 **O(log n)**。由于篇幅,这里不展开完整删除代码;理解插入的三种情况后,删除的四种情况是同一套“重新着色 + 旋转”逻辑的延伸。

---

### 📊 复杂度与 `std::map` 操作映射

| `std::map` 操作 | 红黑树对应动作 | 复杂度 |
| :--- | :--- | :--- |
| `find` / `count` / `lower_bound` | BST 查找 | O(log n) |
| `insert` / `emplace` | BST 插入 + 修复 | O(log n),最多 2 次旋转 |
| `erase` | BST 删除 + 修复 | O(log n),最多 3 次旋转 |
| `begin()` / `rbegin()` | header 节点直接定位 | **O(1)** |
| `++it` / `--it` | 中序后继/前驱 | 均摊 O(1),最坏 O(log n) |
| 中序遍历全部元素 | 递归/迭代中序 | O(n) |

`std::map` 的迭代器是**双向迭代器**,中序遍历天然有序。`begin()` 是红黑树最左节点,`end()` 是 header,`--end()` 是 header 的右指针指向的最右节点——这是 header 节点设计的直接收益。

---

### 💻 可运行示例一:`std::map` 的有序性与基本操作

```cpp
#include <iostream>
#include <map>
#include <string>

int main() {
std::map<int, std::string> m;
m[30] = "thirty";
m[10] = "ten";
m[20] = "twenty";
m.insert({40, "forty"});
m.emplace(25, "twenty-five");

// 中序遍历:有序输出
std::cout << "有序遍历:" << std::endl;
for (const auto& kv : m) {
std::cout << " " << kv.first << " -> " << kv.second << std::endl;
}

// lower_bound / upper_bound:利用有序性做范围查询
auto lo = m.lower_bound(20);
auto hi = m.upper_bound(30);
std::cout << "区间 [20, 30] 内的键: ";
for (auto it = lo; it != hi; ++it) {
std::cout << it->first << " ";
}
std::cout << std::endl;

// 删除
m.erase(20);
std::cout << "删除 20 后元素数: " << m.size() << std::endl;

return 0;
}
```

**输出**(`std::map` 的遍历顺序是确定的):

```
有序遍历:
10 -> ten
20 -> twenty
25 -> twenty-five
30 -> thirty
40 -> forty
区间 [20, 30] 内的键: 20 25 30
删除 20 后元素数: 4
```

---

### 💻 可运行示例二:简化红黑树,验证插入修复

下面是一个精简但完整的红黑树实现(仅插入,带性质验证),用于直观展示旋转和重新着色。它**不是** `std::map` 的实现,但核心逻辑与 libstdc++ 的 `insertFixup` 一致:

```cpp
#include <iostream>
#include <vector>

enum class Color { RED, BLACK };

struct Node {
int key;
Color color;
Node* left;
Node* right;
Node* parent;
explicit Node(int k) : key(k), color(Color::RED),
left(nullptr), right(nullptr), parent(nullptr) {}
};

class RBTree {
Node* root;
Node* NIL; // 黑色哨兵叶子

void leftRotate(Node* x) {
Node* y = x->right;
x->right = y->left;
if (y->left != NIL) y->left->parent = x;
y->parent = x->parent;
if (x->parent == NIL) root = y;
else if (x == x->parent->left) x->parent->left = y;
else x->parent->right = y;
y->left = x;
x->parent = y;
}

void rightRotate(Node* y) {
Node* x = y->left;
y->left = x->right;
if (x->right != NIL) x->right->parent = y;
x->parent = y->parent;
if (y->parent == NIL) root = x;
else if (y == y->parent->left) y->parent->left = x;
else y->parent->right = x;
x->right = y;
y->parent = x;
}

void insertFixup(Node* z) {
while (z->parent->color == Color::RED) {
if (z->parent == z->parent->parent->left) {
Node* y = z->parent->parent->right;
if (y->color == Color::RED) {
z->parent->color = Color::BLACK;
y->color = Color::BLACK;
z->parent->parent->color = Color::RED;
z = z->parent->parent;
} else {
if (z == z->parent->right) {
z = z->parent;
leftRotate(z);
}
z->parent->color = Color::BLACK;
z->parent->parent->color = Color::RED;
rightRotate(z->parent->parent);
}
} else {
Node* y = z->parent->parent->left;
if (y->color == Color::RED) {
z->parent->color = Color::BLACK;
y->color = Color::BLACK;
z->parent->parent->color = Color::RED;
z = z->parent->parent;
} else {
if (z == z->parent->left) {
z = z->parent;
rightRotate(z);
}
z->parent->color = Color::BLACK;
z->parent->parent->color = Color::RED;
leftRotate(z->parent->parent);
}
}
}
root->color = Color::BLACK;
}

int blackHeight(Node* n, bool& ok) {
if (n == NIL) return 1;
int lh = blackHeight(n->left, ok);
int rh = blackHeight(n->right, ok);
if (lh != rh) ok = false;
if (n->color == Color::RED &&
(n->left->color == Color::RED || n->right->color == Color::RED)) {
ok = false;
}
return lh + (n->color == Color::BLACK ? 1 : 0);
}

void inorder(Node* n) {
if (n == NIL) return;
inorder(n->left);
std::cout << n->key << (n->color == Color::RED ? "(R) " : "(B) ");
inorder(n->right);
}

public:
RBTree() {
NIL = new Node(0);
NIL->color = Color::BLACK;
NIL->left = NIL->right = NIL->parent = NIL;
root = NIL;
}

void insert(int key) {
Node* z = new Node(key);
Node* y = NIL;
Node* x = root;
while (x != NIL) {
y = x;
if (z->key < x->key) x = x->left;
else x = x->right;
}
z->parent = y;
if (y == NIL) root = z;
else if (z->key < y->key) y->left = z;
else y->right = z;
z->left = z->right = NIL;
insertFixup(z);
}

void printInorder() {
inorder(root);
std::cout << std::endl;
}

bool validate() {
if (root->color != Color::BLACK) return false;
bool ok = true;
blackHeight(root, ok);
return ok;
}
};

int main() {
RBTree tree;
for (int k : {10, 20, 30, 15, 25, 5, 1, 40, 35, 50}) {
tree.insert(k);
}
std::cout << "中序遍历(R=红, B=黑): ";
tree.printInorder();
std::cout << "红黑树性质验证: "
<< (tree.validate() ? "通过" : "失败") << std::endl;
return 0;
}
```

**一段可能的输出**(树的形状取决于插入顺序和实现细节,颜色分布可能不同,但性质验证必须通过):

```
中序遍历(R=红, B=黑): 1(R) 5(B) 10(R) 15(R) 20(B) 25(R) 30(B) 35(R) 40(B) 50(R)
红黑树性质验证: 通过
```

这段代码的价值在于:你可以修改插入序列、在 `insertFixup` 中加打印,观察**情况 1 的重新着色**和**情况 2/3 的旋转**如何被触发。中序遍历始终有序,验证了红黑树作为 BST 的本质;性质验证通过,说明平衡约束被正确维护。

---

### ⚠️ 使用 `std::map` 时与红黑树相关的注意事项

- **不要依赖内部结构**:C++ 标准只保证 `std::map` 的复杂度,不保证红黑树。你的代码不应假设节点颜色、header 布局或具体旋转行为。
- **迭代器稳定性**:红黑树插入不会使其他节点的迭代器失效(只可能触发旋转,节点地址不变);删除只使指向被删元素的迭代器失效。这是 `std::map` 相比 `unordered_map`(rehash 导致全部迭代器失效)的一大优势。
- **`operator[]` 会插入**:与红黑树无关,但 `std::map` 的 `operator[]` 在键不存在时插入默认值,只读场景应使用 `find` 或 `at`。
- **比较函数必须严格弱序**:红黑树的 BST 性质依赖 `std::less<Key>` 的严格弱序。自定义比较器若违反严格弱序,树结构会被破坏,查找和遍历结果未定义。
- **节点开销**:每个节点除键值对外,还要存颜色位和三个指针(父、左、右),内存开销高于 `std::unordered_map` 的节点(后者通常只有 next 指针)。元素数量很大且内存敏感时需权衡。

---

### 💎 小结

`std::map` 的红黑树是一棵**带颜色位的二叉搜索树**,通过“红节点不能相邻”和“黑高一致”两条约束维持近似平衡,把树高控制在 `2 log n` 以内。它用**常数次旋转**完成插入和删除后的修复,在读写混合场景下比 AVL 树更实用。理解它的最佳路径是**2-3-4 树等价视角**:重新着色对应节点分裂,旋转对应多键节点内部调整。上面的简化实现展示了插入修复的完整逻辑,而 `std::map` 在这个骨架上增加了 header 节点、迭代器封装和内存分配器,对外提供有序、稳定、O(log n) 的关联容器接口。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进