在数据结构的世界里,平衡二叉搜索树是高效查找、插入和删除操作的核心。其中,红黑树以其近似平衡的特性和较低的旋转开销,成为工业界的 “宠儿”—— 从 C++ STL 的map/set到 Linux 内核的内存管理,都能看到它的身影。本文将以 “概念→原理→实现” 为脉络,带你彻底掌握红黑树的设计思想与代码落地。
一、红黑树是什么?核心规则与平衡原理
红黑树本质是带颜色约束的二叉搜索树:它在每个节点中增加一个 “颜色” 字段(红色或黑色),通过 4 条规则确保树的 “近似平衡”—— 没有任何一条从根到叶子的路径长度会超过其他路径的 2 倍。
1.1 红黑树的 4 条核心规则
这是红黑树的 “宪法”,所有操作都围绕维护这些规则展开:
- 颜色约束:每个节点要么是红色,要么是黑色,不存在其他颜色。
- 根节点特性:整棵树的根节点必须是黑色(确保最顶层路径的稳定性)。
- 红色节点限制:若一个节点是红色,其两个子节点必须是黑色(禁止连续的红色节点,避免路径过长)。
- 黑色节点平衡:对于任意节点,从该节点到其所有 “空叶子”(NIL 节点,可理解为虚拟的空节点)的路径上,黑色节点的数量完全相同(这是平衡的关键)。
补充说明:《算法导论》中提到 “所有 NIL 节点(空叶子)为黑色”,这是规则 4 的辅助定义,目的是统一路径的终点判断,实际实现中可简化处理。
1.2 为什么红黑树能保证 “近似平衡”?
红黑树的平衡并非像 AVL 树那样严格控制 “左右子树高度差≤1”,而是通过颜色规则间接实现:
最短路径:全由黑色节点组成(根据规则 4,所有路径的黑色节点数相同,全黑路径是理论最短路径),设其长度为bh(黑色高度)。
最长路径:黑红节点交替出现(根据规则 3,不能有连续红色节点),长度为2*bh(每个黑色节点后接一个红色节点)。
由此可得核心关系:bh ≤ 树高h ≤ 2*bh,这意味着最长路径不会超过最短路径的 2 倍,保证了 “近似平衡”。
1.3 红黑树的效率:为何比 AVL 树更实用?
红黑树和 AVL 树的时间复杂度均为O(log N)(N为节点数),但红黑树在工业界更常用,原因在于:
旋转开销更低:AVL 树要求 “严格平衡”,插入 / 删除时可能需要多次旋转;红黑树仅需 “近似平衡”,平均旋转次数更少(插入最多 2 次旋转,删除最多 3 次)。
实现成本可控:红黑树通过颜色约束简化平衡维护,而 AVL 树需要维护 “平衡因子”,代码复杂度更高。
二、红黑树的结构设计
在实现红黑树前,我们需要先定义节点结构和树的整体框架。红黑树的节点除了二叉搜索树的 “键值对、左右子节点”,还需增加父节点指针(用于向上回溯维护规则)和颜色字段。
2.1 节点与树的代码定义(C++ 模板实现)
// 1. 枚举节点颜色
enum Colour {
RED, // 红色节点
BLACK // 黑色节点
};
// 2. 红黑树节点结构(键值对类型,支持泛型)
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) // 插入时默认红色(后续解释原因)
{}
};
// 3. 红黑树类框架
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node; // 简化节点类型名
public:
// 后续实现插入、查找、验证等接口
bool Insert(const pair<K, V>& kv);
Node* Find(const K& key);
bool IsBalance();
private:
Node* _root = nullptr; // 根节点,初始为空
// 辅助函数:旋转(与AVL树一致,无需平衡因子)
void RotateL(Node* parent); // 左旋转
void RotateR(Node* parent); // 右旋转
};
关键细节:插入节点默认设为红色,而非黑色。原因是:若插入黑色节点,会直接破坏 “规则 4”(某条路径的黑色节点数增加),而插入红色节点仅可能破坏 “规则 3”(连续红色节点),后者的修复成本更低。
三、红黑树的插入:最核心的操作
红黑树的插入分为两步:先按二叉搜索树规则插入节点,再根据 “父节点颜色” 判断是否破坏规则,通过 “变色” 或 “旋转 + 变色” 修复。
插入后仅需关注一种异常场景:父节点(p)为红色(若父节点为黑色,无规则破坏,插入直接结束)。此时根据 “祖父节点(g)的另一个子节点(叔叔 u)” 的颜色,分为 3 种处理情况。
3.1 前置约定
为了清晰描述场景,定义以下符号:
cur:新插入的节点(红色)。
p:cur的父节点(红色,触发异常的原因)。
g:p的父节点(黑色,因p是红色,根据规则 3,g必为黑色)。
u:p的兄弟节点(g的另一个子节点,颜色是判断场景的关键)。
3.2 插入场景 1:叔叔 u 存在且为红色 → 仅需变色
场景分析
p和u均为红色,g为黑色。此时cur与p连续红色(破坏规则 3),但可通过 “变色” 修复:
将p和u改为黑色(消除连续红色,同时补充黑色节点数量)。
将g改为红色(维持 “规则 4”:g所在子树的黑色节点数不变)。
注意事项
变色后g变为红色,若g的父节点也是红色,会再次触发 “连续红色” 问题。因此需要将cur更新为g,继续向上回溯调整,直到g是根节点(此时需将g改回黑色,符合规则 2)。
示意图(抽象模型)
g(黑) → 变色后 g(红)
/ \ / \
p(红) u(红) → p(黑) u(黑)
/
cur(红)
3.3 插入场景 2:叔叔 u 不存在或为黑色 → 单旋 + 变色
场景分析
u不存在(cur是叶子节点)或u为黑色,此时单纯变色无法修复规则,需通过 “旋转” 调整树结构,再配合变色。
根据p是g的左 / 右子节点、cur是p的左 / 右子节点,分为两种子场景:
- p 是 g 的左子节点,cur 是 p 的左子节点:以
g为旋转中心,执行右单旋,然后将p改为黑色、g改为红色。 - p 是 g 的右子节点,cur 是 p 的右子节点:以
g为旋转中心,执行左单旋,然后将p改为黑色、g改为红色。
核心目的
旋转后p成为新的子树根节点(黑色),g成为p的子节点(红色),既消除了连续红色,又维持了黑色节点数量平衡,且无需继续向上调整(p是黑色,其父节点颜色不影响规则)。
示意图(右单旋案例)
g(黑) → 右旋后 p(黑)
/ → / \
p(红) cur(红) g(红)
/
cur(红)
3.4 插入场景 3:叔叔 u 不存在或为黑色 → 双旋 + 变色
场景分析
与场景 2 的区别是cur的位置:p是g的左子节点时cur是p的右子节点,或p是g的右子节点时cur是p的左子节点。此时单旋无法直接修复,需先 “调整cur和p的位置”,再执行单旋。
以 “p是g的左子节点,cur是p的右子节点” 为例:
- 以
p为旋转中心,执行左单旋(将cur提升为p的父节点)。 - 以
g为旋转中心,执行右单旋(将cur提升为新的子树根节点)。 - 将
cur改为黑色、g改为红色。
核心目的
双旋本质是 “将cur调整到场景 2 的位置”,再用场景 2 的逻辑修复,最终效果与场景 2 一致:消除连续红色,维持黑色节点平衡。
示意图(左旋 + 右旋案例)
g(黑) g(黑) cur(黑)
/ / / \
p(红) → cur(红) → p(红) g(红)
\ /
cur(红) p(红)
3.5 插入操作的完整代码实现
// 右单旋(与AVL树一致,无需维护平衡因子)
template<class K, class V>
void RBTree<K, V>::RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 调整指针:subLR成为parent的左子节点
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
// 调整指针:parent成为subL的右子节点
subL->_right = parent;
Node* pParent = parent->_parent;
parent->_parent = subL;
// 调整subL与祖父节点的关系
if (pParent == nullptr) {
_root = subL; // parent是根节点,subL成为新根
} else {
if (pParent->_left == parent) {
pParent->_left = subL;
} else {
pParent->_right = subL;
}
subL->_parent = pParent;
}
}
// 左单旋(逻辑与右单旋对称)
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;
subR->_left = parent;
Node* pParent = parent->_parent;
parent->_parent = subR;
if (pParent == nullptr) {
_root = subR;
} else {
if (pParent->_left == parent) {
pParent->_left = subR;
} else {
pParent->_right = subR;
}
subR->_parent = pParent;
}
}
// 插入核心逻辑
template<class K, class V>
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
// 1. 空树处理:根节点为黑色
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
// 2. 按二叉搜索树规则找到插入位置
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; // 键已存在,插入失败
}
}
// 3. 插入新节点(默认红色)
cur = new Node(kv);
cur->_col = RED;
if (parent->_kv.first < kv.first) {
parent->_right = cur;
} else {
parent->_left = cur;
}
cur->_parent = parent;
// 4. 维护红黑树规则:父节点为红色时需调整
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent; // g必存在(p是红,g是黑)
// 情况:p是g的左子节点
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right; // 获取叔叔u
// 子场景1:u存在且为红色 → 变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
// 向上回溯:g变为红,需检查其父亲
cur = grandfather;
parent = cur->_parent;
} else {
// 子场景2/3:u不存在或为黑 → 旋转+变色
if (cur == parent->_left) {
// 子场景2:cur是p的左 → 右单旋
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
// 子场景3:cur是p的右 → 双旋(左+右)
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 旋转后无需继续向上调整
}
} else {
// 情况:p是g的右子节点(与左子节点逻辑对称)
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
// 子场景1:u存在且为红 → 变色
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
// 子场景2/3:u不存在或为黑 → 旋转+变色
if (cur == parent->_right) {
// 子场景2:cur是p的右 → 左单旋
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
// 子场景3:cur是p的左 → 双旋(右+左)
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
// 最终确保根节点是黑色(防止回溯后根变为红色)
_root->_col = BLACK;
return true;
}
四、红黑树的查找与验证
插入实现后,我们需要两个辅助功能:查找(验证二叉搜索树特性)和验证(确保红黑树规则未被破坏)。
4.1 查找操作:复用二叉搜索树逻辑
红黑树的查找与普通二叉搜索树完全一致,时间复杂度为O(log N):
template<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::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; // 键不存在
}
4.2 验证操作:检查红黑树规则
验证的核心是检查前文提到的 4 条规则,重点是 “无连续红色节点” 和 “黑色节点数量平衡”:
// 辅助函数:递归检查每个节点的规则
template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum) {
// 规则4:空节点(路径终点),检查黑色节点数是否与参考值一致
if (root == nullptr) {
if (blackNum != refNum) {
cout << "错误:存在黑色节点数量不相等的路径" << endl;
return false;
}
return true;
}
// 规则3:检查当前节点是否与父节点连续红色
if (root->_col == RED && root->_parent->_col == RED) {
cout << "错误:节点" << root->_kv.first << "存在连续红色节点" << endl;
return false;
}
// 累计黑色节点数量
if (root->_col == BLACK) {
blackNum++;
}
// 递归检查左右子树
return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}
// 对外接口:启动验证
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 refNum = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) {
refNum++;
}
cur = cur->_left;
}
// 递归检查所有路径
return Check(_root, 0, refNum);
}
五、总结与延伸
红黑树的核心是 “用颜色规则换平衡”:通过牺牲 AVL 树的 “严格平衡”,换取更低的旋转开销,同时保证O(log N)的时间复杂度。本文重点讲解了红黑树的插入操作,而删除操作因逻辑更复杂(涉及双黑色节点修复),建议参考《算法导论》或《STL 源码剖析》深入学习。
1118

被折叠的 条评论
为什么被折叠?



