摘要 :接前文。红黑树是面试高频考点,也是 STL
map/set的底层数据结构。本文用 C++ 模板从零实现红黑树的插入操作,逐一拆解叔叔为红(变色)、叔叔为黑(四种旋转)的调整过程,配合图示和完整可运行的测试代码,帮你真正理解红黑树的自平衡机制。
前言:为什么要学红黑树?
把 1 到 10 按顺序插入一棵二叉搜索树会怎样?答案是你会得到一条链表,查找效率从 O(log n) 退化到 O(n)。

这就是普通 BST 的致命缺陷:它的性能完全取决于插入顺序。为了解决这个问题,人们发明了自平衡二叉搜索树。常见的有两种:
- AVL 树:严格平衡(左右子树高度差不超过 1),查找快,但插入/删除时旋转频繁
- 红黑树:近似平衡(最长路径不超过最短路径的 2 倍),工程上更实用,旋转次数更少
你每次用 std::map 或 std::set 的时候,底层都在帮你维护一棵红黑树。
1. 红黑树的五条性质
红黑树在普通 BST 的基础上给每个节点加了一个"颜色"属性(红或黑),然后通过以下五条规则来约束树的形状:
| 性质 | 内容 | 约束了什么 |
|---|---|---|
| 1 | 节点非红即黑 | 状态空间有限,便于编码 |
| 2 | 根节点是黑色 | 插入调整的终止条件 |
| 3 | 所有叶子(NIL)是黑色 | 统一黑高计算的边界 |
| 4 | 红色节点的子节点必须是黑色(不能连续红色) | 限制最长路径长度 |
| 5 | 从任一节点到其所有叶子路径包含相同数量的黑色节点(黑高一致) | 保证近似平衡 |
其中,性质 4 和性质 5 联合保证了"最长路径 ≤ 2 × 最短路径"。
最短路径是全黑节点(假设有 B 个黑节点),最长路径是红黑交替(B 个黑 + B 个红 = 2B)。所以红黑树的高度最多是 2log(n+1),查找效率始终是 O(log n)。
插入调整就来自这两条性质:新插入的节点默认为红色,这不影响性质 5(黑高没变),但可能违反性质 4(跟红色父节点相邻)。所以插入后的调整,本质上就是在修复"连续红色"的问题,同时不破坏黑高一致性。

对应代码中的颜色定义:
cpp
enum NodeColor {
red,
black
};
2. 节点结构与树的框架
节点结构
红黑树的节点跟普通 BST 相比,多了两样东西:颜色 和指向父节点的指针。
cpp
template<class K, class V>
struct RBTreeNode
{
using Node = RBTreeNode<K, V>;
std::pair<K, V> _kv; // 键值对
Node* _left; // 左子节点
Node* _right; // 右子节点
Node* _parent; // 父节点
NodeColor _color; // 红 or 黑
RBTreeNode(const std::pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _color(red) // 新节点默认为红色
{}
};

这里有三个设计决策值得说明:
为什么需要 _parent 指针? 普通 BST 只需要 _left 和 _right 就够了,但红黑树的插入调整需要向上回溯到祖父节点甚至更高层,没有 _parent 指针就做不到。
为什么新节点默认红色? 因为插入一个黑色节点一定 违反性质 5(黑高不一致),而插入红色节点只可能违反性质 4(连续红色),后者更容易修复。
树的框架
cpp
template<class K, class V>
class RBTree {
public:
using Node = RBTreeNode<K, V>;
RBTree() : _root(nullptr) {}
void Insert(const std::pair<K, V>& kv);
bool IsBalance();
bool IsValidRBTree();
private:
Node* _root;
void RotateL(Node* parent); // 左旋
void RotateR(Node* parent); // 右旋
};
3. 插入操作:先找位置,再调整颜色
红黑树的插入分为两个阶段:
- BST 标准插入:沿树查找合适位置,挂载新节点
- 颜色调整:向上检查,通过变色或旋转修复可能违反的性质

阶段 1:BST 查找 + 挂载
这部分跟普通 BST 完全一样,没什么特别的:
cpp
void Insert(const std::pair<K, V>& kv) {
// 空树:直接创建黑色根节点
if (_root == nullptr) {
_root = new Node(kv);
_root->_color = black;
return;
}
// BST 查找插入位置
Node* cur = _root;
Node* parent = nullptr;
while (cur) {
if (kv.first < cur->_kv.first) {
parent = cur;
cur = cur->_left;
}
else if (kv.first > cur->_kv.first) {
parent = cur;
cur = cur->_right;
}
else {
return; // 键已存在,不插入
}
}
// 挂载新节点(红色)
Node* newnode = new Node(kv);
if (newnode->_kv.first < parent->_kv.first)
parent->_left = newnode;
else
parent->_right = newnode;
newnode->_parent = parent;
// 阶段 2:进入调整循环...
阶段 2:调整循环
调整的核心是一个 while 循环,只要"当前节点的父节点是红色",就说明出现了连续红色(违反性质 4),需要继续调整:
cpp
while (newnode->_parent && newnode->_parent->_color == red)
{
Node* father = newnode->_parent;
Node* grandfa = father->_parent;
Node* uncle = (grandfa->_left == father)
? grandfa->_right
: grandfa->_left;
// 根据 uncle 的颜色决定:变色 or 旋转
// ...
}
_root->_color = black; // 保底:根节点始终为黑色
调整过程中有三个关键角色:
#mermaid-svg-cBxGNiRN2n1jYN2f{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cBxGNiRN2n1jYN2f .error-icon{fill:#552222;}#mermaid-svg-cBxGNiRN2n1jYN2f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cBxGNiRN2n1jYN2f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cBxGNiRN2n1jYN2f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cBxGNiRN2n1jYN2f .marker.cross{stroke:#333333;}#mermaid-svg-cBxGNiRN2n1jYN2f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cBxGNiRN2n1jYN2f p{margin:0;}#mermaid-svg-cBxGNiRN2n1jYN2f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster-label text{fill:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster-label span{color:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster-label span p{background-color:transparent;}#mermaid-svg-cBxGNiRN2n1jYN2f .label text,#mermaid-svg-cBxGNiRN2n1jYN2f span{fill:#333;color:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f .node rect,#mermaid-svg-cBxGNiRN2n1jYN2f .node circle,#mermaid-svg-cBxGNiRN2n1jYN2f .node ellipse,#mermaid-svg-cBxGNiRN2n1jYN2f .node polygon,#mermaid-svg-cBxGNiRN2n1jYN2f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cBxGNiRN2n1jYN2f .rough-node .label text,#mermaid-svg-cBxGNiRN2n1jYN2f .node .label text,#mermaid-svg-cBxGNiRN2n1jYN2f .image-shape .label,#mermaid-svg-cBxGNiRN2n1jYN2f .icon-shape .label{text-anchor:middle;}#mermaid-svg-cBxGNiRN2n1jYN2f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cBxGNiRN2n1jYN2f .rough-node .label,#mermaid-svg-cBxGNiRN2n1jYN2f .node .label,#mermaid-svg-cBxGNiRN2n1jYN2f .image-shape .label,#mermaid-svg-cBxGNiRN2n1jYN2f .icon-shape .label{text-align:center;}#mermaid-svg-cBxGNiRN2n1jYN2f .node.clickable{cursor:pointer;}#mermaid-svg-cBxGNiRN2n1jYN2f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cBxGNiRN2n1jYN2f .arrowheadPath{fill:#333333;}#mermaid-svg-cBxGNiRN2n1jYN2f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cBxGNiRN2n1jYN2f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cBxGNiRN2n1jYN2f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cBxGNiRN2n1jYN2f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cBxGNiRN2n1jYN2f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cBxGNiRN2n1jYN2f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster text{fill:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f .cluster span{color:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cBxGNiRN2n1jYN2f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cBxGNiRN2n1jYN2f rect.text{fill:none;stroke-width:0;}#mermaid-svg-cBxGNiRN2n1jYN2f .icon-shape,#mermaid-svg-cBxGNiRN2n1jYN2f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cBxGNiRN2n1jYN2f .icon-shape p,#mermaid-svg-cBxGNiRN2n1jYN2f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cBxGNiRN2n1jYN2f .icon-shape .label rect,#mermaid-svg-cBxGNiRN2n1jYN2f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cBxGNiRN2n1jYN2f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cBxGNiRN2n1jYN2f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cBxGNiRN2n1jYN2f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-cBxGNiRN2n1jYN2f .blackNode>*{fill:#111!important;color:#fff!important;stroke:#111!important;stroke-width:2px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .blackNode span{fill:#111!important;color:#fff!important;stroke:#111!important;stroke-width:2px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .blackNode tspan{fill:#fff!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .redNode>*{fill:#d32f2f!important;color:#fff!important;stroke:#b71c1c!important;stroke-width:2px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .redNode span{fill:#d32f2f!important;color:#fff!important;stroke:#b71c1c!important;stroke-width:2px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .redNode tspan{fill:#fff!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .uncleNode>*{fill:#fff!important;color:#111!important;stroke:#666!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .uncleNode span{fill:#fff!important;color:#111!important;stroke:#666!important;stroke-width:2px!important;stroke-dasharray:5 5!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .uncleNode tspan{fill:#111!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .noteNode>*{fill:#fff!important;color:#333!important;stroke:none!important;font-size:12px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .noteNode span{fill:#fff!important;color:#333!important;stroke:none!important;font-size:12px!important;}#mermaid-svg-cBxGNiRN2n1jYN2f .noteNode tspan{fill:#333!important;} G
grandfa
F
father
U
uncle
?
N*
newnode
grandfa: 祖父节点(黑色)
father: 父节点(红色)
uncle: 叔叔节点(红 / 黑 / 不存在)
newnode: 新节点(红色)
- father:当前节点的父节点(红色,否则不会进入循环)
- grandfa:祖父节点(一定存在且为黑色,因为 father 是红色)
- uncle:叔叔节点(father 的兄弟,可能为红、为黑、或不存在)
叔叔的颜色决定了我们用哪种策略来修复:
- 叔叔为红色 → 变色
- 叔叔为黑色或不存在 → 旋转 + 变色
4. 调整场景一:叔叔存在且为红色(变色)
这是最简单的调整情况。
思路
父亲和叔叔都是红色,祖父是黑色。我们把父亲和叔叔都变黑,祖父变红。这样做的效果是:
- 经过祖父的路径上,黑色节点数量没变(之前是黑祖父 + 红父/叔,现在是红祖父 + 黑父/叔)
- 当前节点和父节点不再连续红色了
但祖父变红之后,可能又跟曾祖父 形成连续红色,所以要把 newnode 上移到祖父位置,继续检查。
实例演示
以插入序列 {10, 5, 15, 3} 为例:
插入 3 之前,树长这样:10(B) 是根,左右子节点 5® 和 15®。
插入 3 之后,3 挂在 5 的左边,3 和 5 都是红色,连续红色!此时叔叔 15 也是红色,触发变色。
变色前:
10(B)
/ \
5(R) 15(R)
/
3(R) ← 新插入
变色后:
10(B) ← 根节点保持黑色
/ \
5(B) 15(B) ← 父和叔叔变黑
/
3(R)

对应代码
cpp
if (uncle && uncle->_color == red) {
father->_color = black;
uncle->_color = black;
if (grandfa == _root) {
return; // 祖父是根,不需要变红(根最终会被染黑)
}
else {
grandfa->_color = red;
newnode = grandfa; // 上移,继续检查
}
}
注意这里的优化:如果祖父已经是根节点,直接返回即可,不需要把根变红再变回黑。
5. 调整场景二:叔叔不存在或为黑色(旋转 + 变色)
这是红黑树插入最核心也最复杂的部分。当叔叔不存在或为黑色时,单纯变色无法解决问题,需要通过旋转来重新平衡。
根据父节点在祖父的左/右,以及新节点在父节点的左/右,一共有四种情况。我们先讲单旋的两种(更简单),再讲双旋的两种。
情况 1:RR 型 ------ 左旋祖父
触发条件 :父在祖父右 边,新节点在父的右边。
数据演示 :插入 {1, 2, 3}
1(B)
\
2(R)
\
3(R) ← 新插入
此时 uncle 不存在(或为黑色 NIL),father(2) 在 grandfa(1) 右边,newnode(3) 在 father(2) 右边 ------ RR 型。
操作:左旋祖父(1),然后 father(2) 变黑,grandfa(1) 变红。
2(B)
/ \
1(R) 3(R)

对应代码:
cpp
if (father == grandfa->_right && newnode == father->_right)
{
RotateL(grandfa); // 左旋祖父
father->_color = black; // 父变黑
grandfa->_color = red; // 祖父变红
}
情况 2:LL 型 ------ 右旋祖父
触发条件 :父在祖父左 边,新节点在父的左边。跟 RR 完全对称。
数据演示 :插入 {3, 2, 1}
3(B)
/
2(R)
/
1(R) ← 新插入
操作:右旋祖父(3),然后 father(2) 变黑,grandfa(3) 变红。
2(B)
/ \
1(R) 3(R)

对应代码:
cpp
else if (father == grandfa->_left && newnode == father->_left)
{
grandfa->_color = red;
father->_color = black;
RotateR(grandfa); // 右旋祖父
}
情况 3:LR 型 ------ 先左旋父,再右旋祖父
触发条件 :父在祖父左 边,新节点在父的右边。节点呈"折线"形,单旋搞不定,需要双旋。
数据演示 :插入 {10, 5, 8}
Step 1 - 旋转前:
10(B)
/
5(R)
\
8(R) ← 新插入
Step 2 - 左旋 5 后:
10(B)
/
8(R)
/
5(R)
Step 3 - 右旋 10 后:
8(B)
/ \
5(R) 10(R)

对应代码:
cpp
else if (father == grandfa->_left && newnode == father->_right)
{
RotateL(father); // 先左旋父
RotateR(grandfa); // 再右旋祖父
newnode->_color = black; // 注意:双旋后变色的是 newnode,不是 father!
grandfa->_color = red;
}
情况 4:RL 型 ------ 先右旋父,再左旋祖父
触发条件 :父在祖父右 边,新节点在父的左边。跟 LR 对称。
数据演示 :插入 {10, 15, 12}
Step 1 - 旋转前:
10(B)
\
15(R)
/
12(R) ← 新插入
Step 2 - 右旋 15 后:
10(B)
\
12(R)
\
15(R)
Step 3 - 左旋 10 后:
12(B)
/ \
10(R) 15(R)

对应代码:
cpp
else if (father == grandfa->_right && newnode == father->_left)
{
RotateR(father); // 先右旋父
RotateL(grandfa); // 再左旋祖父
newnode->_color = black; // 双旋后 newnode 变黑
grandfa->_color = red;
}
四种旋转的规律总结
| 类型 | 形状 | 操作 | 变色 |
|---|---|---|---|
| RR | 祖-父-子 右-右 | 左旋祖父 | father 变黑,grandfa 变红 |
| LL | 祖-父-子 左-左 | 右旋祖父 | father 变黑,grandfa 变红 |
| LR | 祖-父-子 左-右 | 先左旋父,再右旋祖父 | newnode 变黑,grandfa 变红 |
| RL | 祖-父-子 右-左 | 先右旋父,再左旋祖父 | newnode 变黑,grandfa 变红 |
关键区别:单旋后变黑的是 father,双旋后变黑的是 newnode。
旋转修复后直接 break 跳出循环,不需要继续向上检查(旋转已经把局部平衡修好了)。
6. 旋转的实现细节
旋转是红黑树(和 AVL 树)的基础操作。理解旋转不要逐行读代码,先抓住三步:
- 谁上位,谁下沉
- 孤儿怎么办(被抛弃的子树重新挂载)
- 别忘了 parent 指针(双向链接都要更新)
左旋 RotateL
左旋的核心动作:右子节点 subR 上位,parent 下沉成 subR 的左子节点。

旋转前 旋转后
P S
/ \ / \
A S →→→ P C
/ \ / \
B C A B
subR(S)上位,取代parent(P)的位置parent(P)下沉,成为subR的左子节点
-subRL(B)是"孤儿",被过继给parent作右子节点
完整代码:
cpp
void RotateL(Node* parent) {
Node* pParent = parent->_parent;
Node* subR = parent->_right;
Node* subRL = subR->_left;
// Step 1: 处理"孤儿" subRL
if (subRL)
subRL->_parent = parent; // 别忘了更新 parent 指针!
// Step 2: subR 上位
if (parent == _root) {
subR->_parent = nullptr;
subR->_left = parent;
_root = subR;
}
else {
subR->_parent = pParent;
if (parent == pParent->_left)
pParent->_left = subR;
else
pParent->_right = subR;
subR->_left = parent;
}
// Step 3: parent 下沉
parent->_parent = subR;
parent->_right = subRL;
}
右旋 RotateR
右旋跟左旋完全镜像:左子节点 subL 上位,parent 下沉成 subL 的右子节点。
旋转前 旋转后
P S
/ \ / \
S C →→→ A P
/ \ / \
A B B C
完整代码:
cpp
void RotateR(Node* parent) {
Node* pParent = parent->_parent;
Node* subL = parent->_left;
Node* subLR = subL->_right;
// Step 1: 处理"孤儿" subLR
if (subLR)
subLR->_parent = parent; // 这行容易忘,忘了会导致死循环或段错误!
// Step 2: subL 上位
if (parent == _root) {
subL->_parent = nullptr;
subL->_right = parent;
_root = subL;
}
else {
subL->_parent = pParent;
subL->_right = parent;
if (parent == pParent->_left)
pParent->_left = subL;
else
pParent->_right = subL;
}
// Step 3: parent 下沉
parent->_parent = subL;
parent->_left = subLR;
}
最容易出 bug 的地方 :subLR->_parent = parent 和 subRL->_parent = parent 这两行。忘记更新"孤儿"的 parent 指针,编译不会报错,但运行时向上回溯会找到错误的祖先节点,导致死循环或段错误。我写的时候就在这里翻过车。
7. 平衡验证:怎么证明你写对了?
写红黑树最大的痛点不是"写不出来",而是"写出来不知道对不对"。验证函数是你最好的学习工具------先写验证,再写插入,每插入一个节点就跑一次验证,比肉眼看树结构可靠得多。
我们的验证函数 _Validate 递归检查三件事:
- BST 有序性:左子节点的键 < 当前节点的键 < 右子节点的键
- 无连续红色:如果当前节点是红色,它的子节点不能也是红色
- 黑高一致:左子树和右子树的黑色节点数量必须相等
cpp
int _Validate(Node* node) {
if (!node) return 1; // NIL 叶子算一个黑节点
// 检查连续红色
if (node->_color == red) {
if ((node->_left && node->_left->_color == red) ||
(node->_right && node->_right->_color == red)) {
std::cout << " [FAIL] Consecutive red nodes at: "
<< node->_kv.first << std::endl;
return -1;
}
}
// 检查 BST 有序性
if ((node->_left && !(node->_left->_kv.first < node->_kv.first)) ||
(node->_right && !(node->_kv.first < node->_right->_kv.first))) {
std::cout << " [FAIL] BST order violation at: "
<< node->_kv.first << std::endl;
return -1;
}
// 递归检查左右子树的黑高
int leftBH = _Validate(node->_left);
int rightBH = _Validate(node->_right);
if (leftBH == -1 || rightBH == -1) return -1;
if (leftBH != rightBH) {
std::cout << " [FAIL] Black-height mismatch at " << node->_kv.first
<< " L=" << leftBH << " R=" << rightBH << std::endl;
return -1;
}
// 当前节点的黑高 = 子树黑高 + (自身是黑色 ? 1 : 0)
return leftBH + (node->_color == black ? 1 : 0);
}
返回值的设计很巧妙:正常时返回该子树的黑高(正整数),出错时返回 -1。这样递归过程中任何一层检测到违规,错误都会向上传播。
8. 测试设计:怎么测一棵红黑树?
写测试不是随便插几个数然后看看"没崩就行"。好的测试要针对每种调整场景构造输入,确保每条代码路径都被覆盖到。
我们设计了 12 个测试用例,覆盖了以下场景:
| 测试名 | 覆盖场景 | 输入数据 |
|---|---|---|
| TestEmptyTree | 边界:空树 | 无插入 |
| TestSingleNode | 边界:单节点,根必须为黑 | {42} |
| TestDuplicateKeys | 边界:重复键拒绝 | {5, 5, 5} |
| TestAscendingInsert | RR 旋转(连续右插入) | 1, 2, 3, ..., 20 |
| TestDescendingInsert | LL 旋转(连续左插入) | 20, 19, ..., 1 |
| TestZigZagInsert | LR/RL 双旋 | {10,5,8} / {10,15,12} |
| TestRecolorCase | 叔叔为红的变色 | {10, 5, 15, 3, 12, 18} |
| TestStringKeys | 泛型验证 | {"alpha", "bravo", ...} |
| TestContinuousValidation | 每 10 次插入验证一次 | 100 个随机数(seed=42) |
| TestStressLarge | 压力测试 | 1000 个随机数(seed=12345) |
| TestRRRotation | RR 旋转单独验证 | {1, 2, 3} |
| TestLLRotation | LL 旋转单独验证 | {3, 2, 1} |
我们挑两个有代表性的展开讲。
升序插入测试(覆盖 RR 旋转)
cpp
void TestAscendingInsert() {
TEST("Ascending Order Insert (1..20)")
RBTree<int, int> rbt;
for (int i = 1; i <= 20; ++i) {
rbt.Insert({i, i * 10});
}
CHECK(rbt.Size() == 20, "size == 20");
CHECK(rbt.IsValidRBTree(), "valid RB tree");
bool allFound = true;
for (int i = 1; i <= 20; ++i) {
auto* v = rbt.Find(i);
if (!v || *v != i * 10) { allFound = false; break; }
}
CHECK(allFound, "all 20 keys found with correct values");
}
按 1 到 20 的升序插入,如果是普通 BST 会退化成链表。但红黑树会不断触发 RR 旋转和变色来保持平衡。这个测试验证了:节点数正确、所有 RB 性质成立、每个键都能找到且值正确。
压力测试(综合验证)
cpp
void TestStressLarge() {
TEST("Stress Test (1000 random insertions)")
RBTree<int, int> rbt;
std::vector<int> keys;
for (int i = 0; i < 1000; ++i) keys.push_back(i);
std::mt19937 rng(12345);
std::shuffle(keys.begin(), keys.end(), rng);
for (int i = 0; i < 1000; ++i)
rbt.Insert({keys[i], keys[i] * 10});
CHECK(rbt.Size() == 1000, "size == 1000");
CHECK(rbt.IsValidRBTree(), "valid RB tree after 1000 insertions");
}
1000 个随机打乱的数插入后,验证所有红黑树性质仍然成立。用固定种子保证每次运行结果一致。

测试框架
测试用了两个简单的宏来统一输出格式:
cpp
#define TEST(name) \
std::cout << "=== " << name << " ===" << std::endl;
#define CHECK(cond, msg) \
do { \
if (cond) { \
std::cout << " [PASS] " << msg << std::endl; \
++g_testsPassed; \
} else { \
std::cout << " [FAIL] " << msg << std::endl; \
++g_testsFailed; \
} \
} while(0)
9. 总结与延伸
回顾插入调整的逻辑

没有实现什么
- 删除操作:比插入更复杂(需要处理双黑节点的情况),计划在下一篇讲
- 迭代器 :可以进一步封装成类似 STL
map/set的接口 - 拷贝/析构:生产级代码需要实现深拷贝和递归析构
参考资料
- 《算法导论》第 13 章 - 红黑树
- 《STL 源码剖析》- 侯捷 - RB-tree 章节
- 可视化工具:Red/Black Tree Visualization (cs.usfca.edu)