引言
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它在每个节点上增加一个存储位表示颜色(红色或黑色),通过对根到叶子路径上节点颜色的约束,确保没有一条路径会比其他路径长出两倍,从而保持树的近似平衡。相比于 AVL 树要求严格的高度差不超过 1,红黑树对平衡的控制更为宽松,因此插入和删除时的旋转次数更少,整体性能在实际应用中往往更优。C++ 标准库中的 std::map、std::set 以及 Linux 内核的调度器、Java 的 HashMap(红黑树部分)等大量场景都采用了红黑树作为底层数据结构。
本文将从红黑树的核心规则出发,详细阐述其如何保证最长路径不超过最短路径的 2 倍,分析其时间复杂度,然后系统讲解红黑树的节点结构、插入算法的完整流程(包括变色、单旋+变色、双旋+变色三种情况),并给出平衡验证方法。
目录
[1.1 红黑树的四大约束](#1.1 红黑树的四大约束)
[1.2 红黑树如何保证平衡](#1.2 红黑树如何保证平衡)
[1.3 红黑树的效率](#1.3 红黑树的效率)
[3.1 插入的宏观流程](#3.1 插入的宏观流程)
[3.2 情况一:叔叔存在且为红色(变色)](#3.2 情况一:叔叔存在且为红色(变色))
[3.3 情况二:叔叔不存在或为黑色(单旋+变色)](#3.3 情况二:叔叔不存在或为黑色(单旋+变色))
[3.4 情况三:叔叔不存在或为黑色(双旋+变色)](#3.4 情况三:叔叔不存在或为黑色(双旋+变色))
[3.5 旋转代码](#3.5 旋转代码)
[3.6 插入完整代码框架](#3.6 插入完整代码框架)
一、红黑树的概念与规则
1.1 红黑树的四大约束
红黑树本质上是一棵二叉搜索树,每个节点额外存储一个颜色属性,且必须满足以下四条规则:
-
节点颜色:每个节点不是红色就是黑色。
-
根节点:根节点必须是黑色。
-
红色节点的子节点:如果一个节点是红色的,那么它的两个孩子节点都必须是黑色的(即任何路径上不允许出现连续的红色节点)。
-
黑色节点数量相同:对于任意一个节点,从该节点出发到其所有后代空节点(NULL,视为黑色)的简单路径上,所经过的黑色节点数量必须相同。
注:《算法导论》中明确将 NULL 节点视为黑色叶子节点(称为 NIL 外部节点),这有助于统一路径计数。但在具体实现中,通常只需在代码逻辑中体现这一规则即可。
1.2 红黑树如何保证平衡
由规则 4 可知,从根到任一空节点的路径上黑色节点数量相同,记这个黑色节点数为 bh(black height)。由规则 2 和规则 3 可知,红色节点不能连续出现,因此最长路径必然由"黑‑红‑黑‑红‑..."交替构成,其长度最多为 2 * bh;而最短路径则可能全部由黑色节点组成,长度为 bh。因此,最长路径不会超过最短路径的 2 倍。
需要注意的是,实际的红黑树不一定同时存在"全黑最短路径"和"一黑一红最长路径",但上述理论界保证了树的高度始终维持在 O(\\log N)。
1.3 红黑树的效率
设红黑树中节点总数为 N,树高为 h。由于最短路径长度至少为 \\log_2(N+1)(满二叉树情形),最长路径至多为 2\\log_2(N+1),因此 h \\le 2\\log_2(N+1),即 h = O(\\log N)。所以红黑树的查找、插入、删除操作的时间复杂度均为 O(\\log N)。
与 AVL 树相比,红黑树对平衡的约束较为宽松,因而在插入和删除时所需的旋转次数更少,整体常数因子更小。实际工程中,红黑树的应用更为广泛。
二、红黑树的节点结构
红黑树节点除了存储键值对(pair<K,V>)和左右孩子指针外,还需要存储父节点指针(便于向上回溯)以及颜色枚举值。
cpp
enum Colour {
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode {
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED) // 新节点默认为红色
{}
};
树的封装类仅需维护根节点指针:
cpp
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
// 插入、查找、验证等接口
private:
Node* _root = nullptr;
};
三、红黑树的插入
红黑树的插入分为三个主要步骤:
-
按二叉搜索树的规则插入新节点。
-
将新节点颜色设为红色(若插入后为根节点则设为黑色)。
-
检查是否违反红黑树规则,若违反则通过变色 和旋转进行调整。
3.1 插入的宏观流程
-
空树:直接插入黑色根节点。
-
非空树:插入红色新节点。若父节点为黑色,则无需调整;若父节点为红色,则违反规则 3(连续红色),需要处理。
设当前新节点为 c(cur),父节点 p(parent),祖父节点 g(grandfather),叔叔节点 u(uncle)。由于 p 为红色,而规则 2 要求根为黑色,因此 g 必然存在且为黑色。调整的关键在于 u 的状态(存在且为红色,或不存在/存在且为黑色)。
3.2 情况一:叔叔存在且为红色(变色)
特征 :u 存在且为红色。
处理 :将 p 和 u 变为黑色,g 变为红色。此时以 g 为根的子树黑色节点数量保持不变(因为原本 p 和 u 红色变黑,g 黑色变红,局部黑色节点数不变),但 g 变为红色后可能与其父节点形成连续红色。因此将 g 视为新的 c,继续向上检查。
特点 :只变色,不旋转。无论 c 是 p 的左还是右,p 是 g 的左还是右,处理方式相同。
cpp
// 伪代码示例
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
3.3 情况二:叔叔不存在或为黑色(单旋+变色)
特征 :u 不存在(即 p 为叶子)或 u 存在且为黑色。此时 c 和 p 的连线方向一致(即 p 是 g 的左且 c 是 p 的左,或 p 是 g 的右且 c 是 p 的右)。
处理:
-
若
p是g的左,c是p的左:对g进行右单旋。 -
若
p是g的右,c是p的右:对g进行左单旋。
旋转后,将 p 变为黑色,g 变为红色。此时 p 成为新的子树根,子树黑色节点数不变且无连续红色,且 p 的父亲(原 g 的父亲)可能是任意颜色,但由于 p 为黑色,不会破坏规则,因此调整结束。
代码示例(以右单旋为例):
cpp
if (cur == parent->_left) {
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
3.4 情况三:叔叔不存在或为黑色(双旋+变色)
特征 :u 不存在或为黑色,且 c 与 p 的连线方向不一致(即 p 是 g 的左,c 是 p 的右;或 p 是 g 的右,c 是 p 的左)。
处理:
-
p是g的左,c是p的右:先对p左单旋,再对g右单旋。 -
p是g的右,c是p的左:先对p右单旋,再对g左单旋。
旋转后,将 c 变为黑色,g 变为红色。此时 c 成为新的子树根,同样保证黑色节点数不变且无连续红色,调整结束。
代码示例(左右双旋):
cpp
if (cur == parent->_right) {
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
3.5 旋转代码
红黑树的旋转与 AVL 树在结构上完全一致,区别在于不需要更新平衡因子,仅需调整父子指针和颜色。以下以右单旋为例(左单旋对称):
cpp
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == parentParent->_left)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
}
3.6 插入完整代码框架
cpp
bool Insert(const pair<K, V>& kv) {
// 1. 空树处理
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 2. BST 插入
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
} else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
} else {
return false;
}
}
cur = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// 3. 调整颜色与旋转
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
// 情况1:叔叔存在且红
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
// 情况2/3:叔叔不存在或黑
if (cur == parent->_left) {
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
} else { // parent == grandfather->_right
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
if (cur == parent->_right) {
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
四、红黑树的查找
查找操作与普通二叉搜索树完全一致,时间复杂度 O(\\log N)。
cpp
Node* Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_kv.first < key)
cur = cur->_right;
else if (cur->_kv.first > key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
五、红黑树的验证
验证一棵树是否为红黑树,必须检查其是否满足全部四条规则:
-
根节点为黑色 :直接判断
_root->_col。 -
无连续红色节点:前序遍历时检查每个红色节点的父节点是否也为红色。
-
每条路径黑色节点数相同 :先选取一条路径(如最左路径)的黑色节点数作为参考值
refNum,然后递归检查每条路径的黑色节点数是否等于refNum。
cpp
bool Check(Node* root, int blackNum, const int refNum) {
if (root == nullptr) {
if (blackNum != refNum) {
cout << "黑色节点数不等" << endl;
return false;
}
return true;
}
if (root->_col == RED && root->_parent->_col == RED) {
cout << "连续红色节点" << endl;
return false;
}
if (root->_col == BLACK)
blackNum++;
return Check(root->_left, blackNum, refNum) &&
Check(root->_right, blackNum, refNum);
}
bool IsBalance() {
if (_root == nullptr) return true;
if (_root->_col == RED) return false;
int refNum = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) refNum++;
cur = cur->_left;
}
return Check(_root, 0, refNum);
}
注意:不能简单地通过"最长路径不超过最短路径的 2 倍"来验证,因为该条件满足时仍可能违反颜色规则(例如红色节点连续但路径长度仍满足 2 倍关系)。
六、关于删除的说明
红黑树的删除操作比插入更为复杂,需要在删除节点后通过变色和旋转恢复红黑树性质。由于篇幅和常见教学安排,本文不展开删除的具体实现,有兴趣的读者可参考《算法导论》或《STL 源码剖析》。
总结
红黑树通过四类颜色约束实现了近似平衡,确保了任何一条路径的长度不超过最短路径的两倍,从而保证查找、插入、删除操作的时间复杂度均为 O(\\log N)。其插入算法以二叉搜索树插入为基础,将新节点设为红色,然后根据叔叔节点的颜色分为三种情形:
-
叔叔为红:仅通过变色处理,并向上传递;
-
叔叔为黑或不存在,且新节点与父节点方向一致:单旋 + 变色;
-
叔叔为黑或不存在,且新节点与父节点方向不一致:双旋 + 变色。
每次旋转和变色都维持了红黑树的性质,且调整后局部子树高度不变,因此无需继续向上更新。
相比于 AVL 树,红黑树牺牲了部分平衡的严格性,换来了更少的旋转次数和更简单的维护逻辑,因此在工业级代码中应用更为广泛。掌握红黑树的核心规则与插入调整策略,是深入理解高效关联容器(如 std::map、std::set)内部实现的关键一步。