C++进阶:(七)红黑树深度解析与 C++ 实现
目录
前言
一、红黑树的核心概念
1.1 红黑树的定义
1.2 红黑树的五大规则
1.3 红黑树的平衡原理
1.4 红黑树的效率分析
二、红黑树的结构设计
2.1 结点结构定义
2.2 红黑树类的框架
2.3 旋转操作实现
2.3.1 右单旋(RotateR)
2.3.2 左单旋(RotateL)
三、红黑树的插入实现
3.1 插入的基础流程
3.2 平衡调整的核心场景
场景 1:叔叔结点存在且为红色(变色调整)
场景 2:叔叔结点不存在或为黑色(单旋 + 变色)
场景 3:叔叔结点不存在或为黑色(双旋 + 变色)
3.3 完整插入代码实现
3.4 插入代码关键要点
四、红黑树的查找与验证实现
4.1 查找操作实现
4.2 红黑树的验证实现
4.2.1 验证辅助函数(Check)
4.2.2 验证主函数(IsBalance)
五、红黑树的应用与扩展
5.1 红黑树的工业应用
5.2 红黑树的扩展(删除操作)
总结
前言
在数据结构与算法领域,平衡二叉搜索树是解决高效查找、插入、删除操作的核心数据结构。AVL 树作为经典的平衡二叉搜索树,通过严格控制左右子树的高度差不超过 1,保证了 O (logN) 的时间复杂度,但频繁的旋转操作导致插入和删除效率偏低。红黑树则通过颜色约束实现 “近似平衡”,在保证 O (logN) 时间复杂度的前提下,大幅减少了旋转次数,成为工业界的首选 ——C++ STL 中的 set、map 等容器底层均采用红黑树实现。
本文将从红黑树的核心概念出发,详细拆解其规则约束、平衡原理、插入逻辑、代码实现及验证方法。下面就让我们正式开始吧!
一、红黑树的核心概念
1.1 红黑树的定义
红黑树是一棵二叉搜索树,在每个结点中增加一个存储位表示颜色(红色或黑色)。通过对从根到叶子的所有路径施加颜色约束,确保没有一条路径的长度超过其他路径的 2 倍,从而实现 “近似平衡”。
二叉搜索树的基础特性:对于任意结点,其左子树中所有结点的关键字均小于该结点的关键字,右子树中所有结点的关键字均大于该结点的关键字。红黑树继承了这一特性,因此查找操作可直接复用二叉搜索树的逻辑。
1.2 红黑树的五大规则
红黑树的平衡特性由以下五条规则严格约束(参考《算法导论》中的定义):
- 每个结点的颜色只能是红色或黑色;
- 根结点必须是黑色;
- 所有叶子结点(包括空结点)必须是黑色;
- 如果一个结点是红色,那么它的两个子结点必须是黑色(无连续红色结点);
- 对于任意一个结点,从该结点到其所有后代 NIL 结点的简单路径上,包含的黑色结点数量相同(黑高一致)。


说明:
- NIL 结点是逻辑上的空结点,用于统一路径处理逻辑,在实际实现中是可以省略的(通过 nullptr 标识),但需在思维上保留该概念;
- 规则 3 和规则 5 是红黑树平衡的核心,规则 4 则是避免路径过长的关键约束。
1.3 红黑树的平衡原理
红黑树通过规则约束,确保最长路径长度不超过最短路径长度的 2 倍,其推导过程如下:
- 定义 “黑高(bh)”:从某结点到其后代 NIL 结点的路径上,黑色结点的数量(不包含当前结点);
- 由规则 5 可知,所有根到 NIL 结点的路径黑高一致,设根结点的黑高为 bh;
- 最短路径:全由黑色结点组成(无红色结点),长度为 bh(路径上的结点数);
- 最长路径:红黑结点交替出现(由规则 4 限制,无连续红色结点),长度为 2*bh(红色结点数 = 黑色结点数);
- 由此可得:最短路径长度 ≤ 任意路径长度 ≤ 2 * 最短路径长度,即红黑树是 “近似平衡” 的。
1.4 红黑树的效率分析
红黑树的时间复杂度由其高度决定,设树中结点数为 N,高度为 h:
- 由二叉搜索树特性:N ≥ 2^bh - 1(全黑路径对应的满二叉树结点数);
- 由最长路径约束:h ≤ 2*bh;
- 推导可得:h ≈ logN,因此查找、插入、删除操作的最坏时间复杂度均为 O (logN)。
与 AVL 树对比:
| 特性 | 红黑树 | AVL 树 |
|---|---|---|
| 平衡条件 | 颜色约束(近似平衡) | 高度差≤1(严格平衡) |
| 旋转次数 | 插入最多 2 次旋转 | 插入最多 2 次旋转 |
| 删除次数 | 删除最多 3 次旋转 | 删除最多 logN 次旋转 |
| 适用场景 | 频繁插入 / 删除的场景 | 频繁查找的场景 |
红黑树的优势在于对平衡的要求相对宽松,因此在插入和删除操作中需要的旋转次数更少,更加适合频繁修改数据的场景。
二、红黑树的结构设计
红黑树的结点需要存储关键字、颜色、左右子指针及父指针(父指针用于向上回溯调整平衡),采用模板类设计以支持任意可比较类型的关键字。
2.1 结点结构定义
// 颜色枚举
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) {} // 新增结点默认红色(关键设计)
};
关键说明:
- 新增结点默认设为红色:若设为黑色,会直接破坏规则 5(黑高一致),而红色结点仅可能破坏规则 4(连续红色结点),后者更容易调整;
- 父指针是红黑树平衡调整的核心:插入或删除后需要向上回溯父结点、祖父结点、叔叔结点的颜色和位置关系。
2.2 红黑树类的框架
红黑树类包含根结点指针,以及插入、查找、验证等核心成员函数:
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:// 右单旋(与AVL树旋转逻辑一致)void RotateR(Node* parent);// 左单旋(与AVL树旋转逻辑一致)void RotateL(Node* parent);// 递归验证红黑树规则bool Check(Node* root, int blackNum, const int refNum);private:Node* _root; // 根结点指针
};
2.3 旋转操作实现
红黑树的旋转操作是与 AVL 树完全一致的,目的是调整子树结构以恢复平衡,无需修改结点颜色(颜色调整单独处理)。旋转操作分为右单旋和左单旋。
2.3.1 右单旋(RotateR)
当左子树过高时,通过右单旋将左子树的根结点提升为新的父结点,原父结点变为新根的右子结点。
template<class K, class V>
void RBTree<K, V>::RotateR(Node* parent) {Node* subL = parent->_left; // 左子树的根结点Node* subLR = subL->_right; // 左子树的右孩子// 1. 处理subLR与parent的关系parent->_left = subLR;if (subLR != nullptr) {subLR->_parent = parent;}// 2. 处理subL与parent父结点的关系Node* parentParent = parent->_parent;if (parentParent == nullptr) {// parent是根结点,旋转后subL成为新根_root = subL;} else if (parent == parentParent->_left) {// parent是左孩子parentParent->_left = subL;} else {// parent是右孩子parentParent->_right = subL;}subL->_parent = parentParent;// 3. 处理subL与parent的关系subL->_right = parent;parent->_parent = subL;
}
2.3.2 左单旋(RotateL)
当右子树过高时,通过左单旋将右子树的根结点提升为新的父结点,原父结点变为新根的左子结点。
template<class K, class V>
void RBTree<K, V>::RotateL(Node* parent) {Node* subR = parent->_right; // 右子树的根结点Node* subRL = subR->_left; // 右子树的左孩子// 1. 处理subRL与parent的关系parent->_right = subRL;if (subRL != nullptr) {subRL->_parent = parent;}// 2. 处理subR与parent父结点的关系Node* parentParent = parent->_parent;if (parentParent == nullptr) {// parent是根结点,旋转后subR成为新根_root = subR;} else if (parent == parentParent->_left) {// parent是左孩子parentParent->_left = subR;} else {// parent是右孩子parentParent->_right = subR;}subR->_parent = parentParent;// 3. 处理subR与parent的关系subR->_left = parent;parent->_parent = subR;
}
旋转操作的要点:
- 旋转时需维护父指针的指向,避免出现悬空指针;
- 旋转仅调整结构,不改变结点颜色,也不破坏二叉搜索树的特性。
三、红黑树的插入实现
红黑树的插入流程分为两步:
① 按二叉搜索树规则插入结点;
② 调整结点颜色和结构,恢复红黑树规则。
3.1 插入的基础流程
- 若树为空,直接创建根结点并设为黑色(满足规则 2);
- 若树非空,按二叉搜索树规则找到插入位置,创建新结点(默认红色)并插入;
- 插入后检查红黑树规则:
- 若新结点的父结点为黑色,规则未被破坏,插入结束;
- 若新结点的父结点为红色,破坏规则 4(连续红色结点),需进行平衡调整。
3.2 平衡调整的核心场景
插入调整的关键在于分析新结点(cur)、父结点(parent)、祖父结点(grandfather)、叔叔结点(uncle)的颜色和位置关系。由于父结点为红色,祖父结点必为黑色(规则 4 不允许连续红色),因此核心变量是叔叔结点的颜色。
为了简化分析,我们定义以下标识:
- cur:新增结点(红色);
- parent:cur 的父结点(红色);
- grandfather:parent 的父结点(黑色);
- uncle:parent 的兄弟结点(祖父结点的另一个子结点)。
根据 parent 是 grandfather 的左孩子或右孩子,以及 uncle 的颜色,下面我们将平衡调整问题分为三大场景,每个场景对应不同的调整策略。
场景 1:叔叔结点存在且为红色(变色调整)
条件:parent 为红,grandfather 为黑,uncle 存在且为红。
问题:cur 与 parent 连续红色(破坏规则 4)。
调整策略:
- 将 parent 和 uncle 设为黑色;
- 将 grandfather 设为红色;
- 把 grandfather 作为新的 cur,向上回溯调整(可能 grandfather 的父结点也是红色)。
调整原理:
- parent 和 uncle 变黑,保证了原路径的黑高不变(满足规则 5);
- grandfather 变红,可能导致其与父结点连续红色,因此需要继续向上调整;
- 若 grandfather 是根结点,调整后需将其设为黑色(满足规则 2)。




代码片段:
// 假设parent是grandfather的左孩子
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {// 场景1:叔叔存在且为红,变色调整parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 向上回溯cur = grandfather;parent = cur->_parent;
}
场景 2:叔叔结点不存在或为黑色(单旋 + 变色)
条件:parent 为红,grandfather 为黑,uncle 不存在或为黑,且 cur 与 parent 同方向(cur 是 parent 的左孩子,parent 是 grandfather 的左孩子;或 cur 是 parent 的右孩子,parent 是 grandfather 的右孩子)。
问题:cur 与 parent 是连续红色的,并且无法通过单纯变色解决(uncle 为黑,变色会破坏规则 5)。
调整策略:
- 以 grandfather 为旋转中心,进行单旋(parent 是左孩子则右旋,parent 是右孩子则左旋);
- 将 parent 设为黑色(新的子树根结点);
- 将 grandfather 设为红色。
调整原理:
- 旋转后 parent 成为子树根结点,设为黑色保证黑高不变;
- grandfather 成为 parent 的子结点,设为红色避免连续红色;
- 调整后无需向上回溯(parent 的父结点若存在,必为黑色,否则之前会被处理)。

代码片段(parent 是 grandfather 的左孩子,cur 是 parent 的左孩子):
else if (cur == parent->_left) {// 场景2:单旋(右旋)+ 变色RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;break; // 调整完成,无需向上回溯
}
代码片段(parent 是 grandfather 的右孩子,cur 是 parent 的右孩子):
else if (cur == parent->_right) {// 场景2:单旋(左旋)+ 变色RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;break;
}
场景 3:叔叔结点不存在或为黑色(双旋 + 变色)
条件:parent 为红,grandfather 为黑,uncle 不存在或为黑,且 cur 与 parent 反方向(cur 是 parent 的右孩子,parent 是 grandfather 的左孩子;或 cur 是 parent 的左孩子,parent 是 grandfather 的右孩子)。
问题:cur 与 parent 连续红色,且单旋无法直接调整(cur 在 parent 的另一侧)。
调整策略:
- 以 parent 为旋转中心,进行一次单旋(cur 是 parent 的右孩子则左旋,cur 是 parent 的左孩子则右旋),将 cur 调整到 parent 的位置;
- 以 grandfather 为旋转中心,进行二次单旋(与场景 2 一致);
- 将 cur 设为黑色(新的子树根结点);
- 将 grandfather 设为红色。
调整原理:
- 第一次旋转将 cur 与 parent 的位置互换,转化为场景 2 的情况;
- 第二次旋转调整结构,cur 设为黑色保证黑高不变;
- 调整后无需向上回溯。

代码片段(parent 是 grandfather 的左孩子,cur 是 parent 的右孩子):
else {// 场景3:双旋(先左旋parent,再右旋grandfather)+ 变色RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;break;
}
代码片段(parent 是 grandfather 的右孩子,cur 是 parent 的左孩子):
else {// 场景3:双旋(先右旋parent,再左旋grandfather)+ 变色RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;break;
}
3.3 完整插入代码实现
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 != nullptr) {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 != nullptr && parent->_col == RED) {Node* grandfather = parent->_parent; // 祖父结点必为黑色if (parent == grandfather->_left) {// 情况A:parent是grandfather的左孩子Node* uncle = grandfather->_right;if (uncle != nullptr && uncle->_col == RED) {// 场景1:叔叔存在且为红,变色调整parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 向上回溯cur = grandfather;parent = cur->_parent;} else {// 场景2和3:叔叔不存在或为黑,旋转+变色if (cur == parent->_left) {// 场景2:cur是parent的左孩子,右单旋RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;} else {// 场景3:cur是parent的右孩子,双旋RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break; // 调整完成,无需向上回溯}} else {// 情况B:parent是grandfather的右孩子(与左孩子对称)Node* uncle = grandfather->_left;if (uncle != nullptr && uncle->_col == RED) {// 场景1:叔叔存在且为红,变色调整parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 向上回溯cur = grandfather;parent = cur->_parent;} else {// 场景2和3:叔叔不存在或为黑,旋转+变色if (cur == parent->_right) {// 场景2:cur是parent的右孩子,左单旋RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;} else {// 场景3:cur是parent的左孩子,双旋RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break; // 调整完成,无需向上回溯}}}// 确保根结点始终为黑色(可能在场景1中被设为红色)_root->_col = BLACK;return true;
}
3.4 插入代码关键要点
- 根结点最终必须设为黑色:场景 1 中可能将根结点设为红色,因此插入结束后需强制将根结点设为黑色;
- 回溯终止条件:parent 为空(cur 成为根结点)或 parent 为黑色(规则 4 不再被破坏);
- 对称处理:parent 是 grandfather 的左孩子和右孩子的逻辑对称,仅需调整旋转方向和叔叔结点的位置;
- 新结点默认红色:这是红黑树插入效率高的关键设计,避免直接破坏规则 5。
四、红黑树的查找与验证实现
4.1 查找操作实现
红黑树的查找逻辑与二叉搜索树完全一致,利用其 “左小右大” 的特性遍历树即可,时间复杂度为 O (logN)。
template<class K, class V>
typename RBTree<K, V>::Node* RBTree<K, V>::Find(const K& key) {Node* cur = _root;while (cur != nullptr) {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(无连续红色结点)和规则 5(黑高一致)。验证流程如下:
- 根结点必须为黑色;
- 前序遍历检查无连续红色结点;
- 计算所有路径的黑高,确保一致。
4.2.1 验证辅助函数(Check)
递归遍历树,检查连续红色结点和黑高一致性:
template<class K, class V>
bool RBTree<K, V>::Check(Node* root, int blackNum, const int refNum) {// 递归到空结点,检查当前路径黑高是否与参考黑高一致if (root == nullptr) {if (blackNum != refNum) {cout << "错误:存在黑高不一致的路径,当前黑高=" << blackNum << ",参考黑高=" << refNum << endl;return false;}return true;}// 检查连续红色结点(当前结点为红,父结点也为红)if (root->_col == RED && root->_parent != nullptr && root->_parent->_col == RED) {cout << "错误:存在连续红色结点,关键字=" << root->_kv.first << endl;return false;}// 遇到黑色结点,黑高计数+1if (root->_col == BLACK) {blackNum++;}// 递归检查左右子树return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}
4.2.2 验证主函数(IsBalance)
计算参考黑高(根结点到最左路径的黑高),调用辅助函数进行验证:
template<class K, class V>
bool RBTree<K, V>::IsBalance() {// 空树视为平衡if (_root == nullptr) {return true;}// 检查根结点是否为黑色if (_root->_col != BLACK) {cout << "错误:根结点不是黑色" << endl;return false;}// 计算参考黑高(根结点到最左路径的黑高)int refNum = 0;Node* cur = _root;while (cur != nullptr) {if (cur->_col == BLACK) {refNum++;}cur = cur->_left;}// 递归检查所有路径int blackNum = 0;return Check(_root, blackNum, refNum);
}
验证函数说明:
- 参考黑高选择根结点到最左路径的黑高,可任意选择一条路径作为参考;
- 递归过程中,blackNum 记录当前路径的黑高,refNum 为参考黑高;
- 若存在连续红色结点或黑高不一致,则直接返回 false 并输出错误信息。

五、红黑树的应用与扩展
5.1 红黑树的工业应用
- C++ STL 容器:set、map、multiset、multimap 的底层实现;
- Linux 内核:进程调度中的 CFS 调度器(使用红黑树管理进程优先级);
- 数据库:MySQL 的 B + 树索引底层使用红黑树维护索引结构;
- 缓存系统:用于高效管理缓存数据的插入、删除和查找。
5.2 红黑树的扩展(删除操作)
红黑树的删除操作比插入更为复杂,核心难点在于删除黑色结点后会破坏规则 5(黑高一致),需要通过 “双重黑色” 结点的概念进行调整。在这我就不详细讲解了,大家感兴趣的话可以参考《算法导论》第 13 章或《STL 源码剖析》的相关内容进行学习。
简单说一下删除操作的核心思路:
- 按二叉搜索树规则删除结点,找到替代结点(中序后继或前驱);
- 若删除的是红色结点,直接删除,规则未被破坏;
- 若删除的是黑色结点,需调整以恢复黑高一致,可能涉及变色、旋转或向上回溯。
总结
红黑树的核心难点在于插入后的平衡调整,需重点掌握三大场景的处理逻辑:叔叔结点为红时的变色调整、叔叔结点为黑或不存在时的单旋 + 变色和双旋 + 变色。通过反复练习和调试代码,相信大家一定可以逐步理解红黑树的平衡原理。感谢大家的支持!

