红黑树:从概念到实现
核心口诀:非空插红;父黑结束;父红看叔;叔红变色上推;叔黑旋转加变色;根最后染黑。
红黑树是一种近似平衡的二叉搜索树 。它不追求 AVL 那样严格的左右高度差约束,而是通过给结点增加"颜色"并约束颜色分布,保证从根到叶子的最长路径不会超过最短路径的 2 倍,从而让插入、查找、删除都保持在 O(log N) 量级。
1. 红黑树的概念
红黑树是一棵二叉搜索树,每个结点额外增加一个存储位表示颜色,颜色只能是红色 或黑色。通过对任意一条从根到 NULL 结点的路径上的颜色进行约束,红黑树保证没有一条路径会比另一条路径长出 2 倍,因此是接近平衡的。
1.1 红黑树的四条规则
| 编号 | 规则 | 说明 |
|---|---|---|
| 1 | 每个结点不是红色就是黑色 | 颜色类型天然保证 |
| 2 | 根结点是黑色 | 空树插入后也要染黑 |
| 3 | 红色结点的两个孩子必须是黑色 | 任意路径不能有连续红结点 |
| 4 | 任意结点到其所有 NULL 结点的简单路径上,黑色结点数量相同 | 即"黑高"一致 |
有些教材会把 NULL 显式看作黑色叶结点,本质与"到 NULL 路径黑结点数相同"一致。本文按原稿说法,把 NULL 作为不存储颜色的外部结点,只参与黑高计数。
1.2 为什么最长路径不超过最短路径的 2 倍?
- 从根到 NULL 的每条路径都有相同数量的黑色结点,设黑高为
bh。 - 极端最短路径:路径上全是黑色结点,长度为
bh。 - 极端最长路径:黑色、红色交替出现,由于不能连续红,最长长度为
2 * bh。 - 因此任意一条从根到 NULL 的路径长度
h满足:
text
bh <= h <= 2 * bh
所以:
text
最长路径 / 最短路径 <= 2
重点 :红黑树不是"绝对平衡",而是"黑高平衡 + 无连续红"共同保证的近似平衡。
2. 红黑树的实现
以下采用 KV 模型的教学版实现:每个结点存储 pair<K, V>,便于理解 map/set 的底层思想。删除操作较复杂,本文聚焦插入、查找与验证。
2.1 红黑树的结构
cpp
#include <iostream>
#include <utility>
using namespace std;
enum Colour {
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode {
pair<K, V> _kv;
Colour _col;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
RBTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _col(RED) // 非空树插入时,新结点默认红色
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
RBTree() : _root(nullptr) {}
bool Insert(const pair<K, V>& kv);
Node* Find(const K& key);
bool IsBalance();
private:
void RotateL(Node* parent);
void RotateR(Node* parent);
bool _Check(Node* root, int blackNum, int refBlackNum);
private:
Node* _root;
};
结构要点 :三叉链结构
left / right / parent便于插入修复时向上回溯;新结点构造时默认红色。
2.2 红黑树的插入
2.2.1 插入的大致流程
- 按二叉搜索树规则找到插入位置并插入。
- 如果是空树插入:新结点作为根,染成黑色。
- 如果是非空树插入:新结点默认红色。
- 插入后检查是否违反规则:
- 如果父亲是黑色:没有违反规则,插入结束。
- 如果父亲是红色:违反"不能连续红",需要根据叔叔结点
u的情况修复。
记:
c:当前结点,curp:父亲,parentg:祖父,grandfatheru:叔叔,uncle
为什么非空插入默认红色?
插入黑色会立刻破坏规则 4,影响整棵树的所有路径,极难维护;插入红色只可能破坏规则 3,且只需局部修复。
2.2.2 情况 1:叔叔存在且为红 ------ 只变色
条件:
text
c 红,p 红,g 黑,u 存在且为红
处理:
text
p 变黑,u 变黑,g 变红
然后把 g 当作新的 c,继续向上更新。
原因 :
p、u变黑后,左右子树路径各增加一个黑结点;g变红相当于保持g所在子树黑结点总数不变,同时解决了c-p连续红的问题。但g变红后可能与g的父亲形成连续红,所以要继续上推。若g是根,最后统一染黑。
特点 :只变色,不旋转;无论 c 是 p 的左还是右,p 是 g 的左还是右,处理方式相同。
2.2.3 情况 2:叔叔为黑或不存在,且同向 ------ 单旋 + 变色
条件:
text
c 红,p 红,g 黑,u 不存在或 u 为黑
并且 c、p、g 同向:
p 是 g 的左,c 是 p 的左;
或 p 是 g 的右,c 是 p 的右。
处理:
| 场景 | 操作 |
|---|---|
p 是 g 的左,c 是 p 的左 |
以 g 为旋转点右单旋,p 变黑,g 变红 |
p 是 g 的右,c 是 p 的右 |
以 g 为旋转点左单旋,p 变黑,g 变红 |
旋转后 p 成为该子树新的根,颜色为黑;g 变红。此时子树黑高不变,没有连续红,且新根 p 的父亲无论是红、黑还是空,都不再违反规则,不需要继续上推。
注意 :
u 不存在时,c一定是新增结点;u 存在且为黑时,c通常不是新增结点,而是情况 1 变色后上推上来的。
2.2.4 情况 3:叔叔为黑或不存在,且异向 ------ 双旋 + 变色
条件与情况 2 类似,但 c、p、g 方向相反:
| 场景 | 操作 |
|---|---|
p 是 g 的左,c 是 p 的右 |
先以 p 为旋转点左单旋,再以 g 为旋转点右单旋;最后 c 变黑,g 变红 |
p 是 g 的右,c 是 p 的左 |
先以 p 为旋转点右单旋,再以 g 为旋转点左单旋;最后 c 变黑,g 变红 |
旋转后 c 成为该子树新的根,颜色为黑;g 变红。同样不需要继续上推。
易错点 :双旋后变色对象是
c和g,不是p和g。可以记作"谁最终成为子树根,谁染黑;原来的祖父染红"。
插入修复情况总结
| 情况 | 叔叔 u |
操作 | 是否继续上推 |
|---|---|---|---|
| 1 | 存在且红 | p、u 变黑,g 变红 |
是,把 g 当新 c |
| 2 | 黑或不存在,且同向 | 单旋 + p 黑、g 红 |
否 |
| 3 | 黑或不存在,且异向 | 双旋 + c 黑、g 红 |
否 |
2.3 红黑树的插入代码实现
cpp
template<class K, class V>
void RBTree<K, V>::RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (ppNode == nullptr) {
_root = subR;
subR->_parent = nullptr;
} else {
if (parent == ppNode->_left)
ppNode->_left = subR;
else
ppNode->_right = subR;
subR->_parent = ppNode;
}
}
template<class K, class V>
void RBTree<K, V>::RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (ppNode == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == ppNode->_left)
ppNode->_left = subL;
else
ppNode->_right = subL;
subL->_parent = ppNode;
}
}
template<class K, class V>
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK; // 空树插入,根必须黑
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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 false; // 不允许重复 key
}
}
cur = new Node(kv); // 非空树插入,新结点默认红色
cur->_parent = parent;
if (kv.first < parent->_kv.first)
parent->_left = cur;
else
parent->_right = cur;
while (parent && parent->_col == RED) {
Node* grand = parent->_parent;
if (parent == grand->_left) {
Node* uncle = grand->_right;
// 情况 1:叔叔存在且为红,只变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grand->_col = RED;
cur = grand;
parent = cur->_parent;
} else {
// 情况 3:异向,双旋 + 变色
if (cur == parent->_right) {
RotateL(parent);
swap(parent, cur);
}
// 情况 2:同向,单旋 + 变色
parent->_col = BLACK;
grand->_col = RED;
RotateR(grand);
break;
}
} else {
Node* uncle = grand->_left;
// 情况 1:叔叔存在且为红,只变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grand->_col = RED;
cur = grand;
parent = cur->_parent;
} else {
// 情况 3:异向,双旋 + 变色
if (cur == parent->_left) {
RotateR(parent);
swap(parent, cur);
}
// 情况 2:同向,单旋 + 变色
parent->_col = BLACK;
grand->_col = RED;
RotateL(grand);
break;
}
}
}
_root->_col = BLACK; // 根最后统一染黑
return true;
}
代码说明 :
swap(parent, cur)是为了把"双旋"统一成"先转成同向,再按单旋处理"。如果不理解,可以拆成两次旋转理解:例如p是g左、c是p右时,先RotateL(p),再RotateR(g)。
2.4 红黑树的查找
红黑树本质仍是二叉搜索树,查找逻辑与普通 BST 相同,时间复杂度为 O(log N)。
cpp
template<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::Find(const K& key) {
Node* cur = _root;
while (cur) {
if (key < cur->_kv.first)
cur = cur->_left;
else if (key > cur->_kv.first)
cur = cur->_right;
else
return cur;
}
return nullptr;
}
2.5 红黑树的验证
不能只检查"最长路径是否不超过最短路径的 2 倍",因为满足这个条件也不一定满足红黑树颜色规则。正确做法是直接检查红黑树的四条规则:
- 颜色类型天然保证每个结点非红即黑。
- 直接检查根是否为黑色。
- 前序遍历检查红色结点:反向检查父亲颜色更方便。
- 前序遍历记录从根到当前结点的黑色结点数量
blackNum;走到 NULL 时与参考黑高比较。
cpp
template<class K, class V>
bool RBTree<K, V>::_Check(Node* root, int blackNum, int refBlackNum) {
if (root == nullptr) {
return blackNum == refBlackNum;
}
// 规则 3:不能有连续红结点。反向检查父亲更方便。
if (root->_col == RED) {
if (root->_parent && root->_parent->_col == RED) {
cout << "出现连续红色结点" << endl;
return false;
}
}
if (root->_col == BLACK)
++blackNum;
return _Check(root->_left, blackNum, refBlackNum)
&& _Check(root->_right, blackNum, refBlackNum);
}
template<class K, class V>
bool RBTree<K, V>::IsBalance() {
if (_root == nullptr)
return true;
// 规则 2:根必须是黑色
if (_root->_col == RED) {
cout << "根结点不是黑色" << endl;
return false;
}
// 取最左路径的黑结点数量作为参考值
int refBlackNum = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK)
++refBlackNum;
cur = cur->_left;
}
return _Check(_root, 0, refBlackNum);
}
验证重点 :规则 4 必须用"参考黑高"逐条路径比较,不能只看某一条路径。
_Check中的blackNum按值传递,保证每条递归路径独立计数。
3. 总结与易错点
| 主题 | 关键结论 |
|---|---|
| 平衡方式 | 黑高一致 + 无连续红,保证最长路径不超过最短路径 2 倍 |
| 插入颜色 | 空树插黑;非空树插红 |
| 父黑 | 直接结束 |
| 父红叔红 | 只变色,g 当新 c 继续上推 |
| 父红叔黑/不存在,同向 | 单旋 + p 黑、g 红 |
| 父红叔黑/不存在,异向 | 双旋 + c 黑、g 红 |
| 根 | 最后必须染黑 |
| 查找 | 与普通 BST 相同 |
| 验证 | 直接检查四条规则,不要只检查路径长度 |
常见易错点:
- 空树插入忘记把根染黑。
- 叔叔为红时错误地旋转;情况 1 只变色。
- 双旋后把
p染黑,正确应是最终成为子树根的c染黑。 - 旋转后忘记维护
parent指针和_root。 - 验证规则 4 时没有使用参考黑高逐路径比较。
- 插入修复结束后忘记执行
_root->_col = BLACK。
与 AVL 树相比,红黑树不追求严格高度平衡,插入/删除时旋转次数更少,查找略慢但整体稳定,因此被广泛用于
std::map、std::set等关联容器。