文章目录
- [第一章:平衡的艺术------从零实现 AVL 树](#第一章:平衡的艺术——从零实现 AVL 树)
-
- [1. AVL 树的"紧箍咒":定义与性质](#1. AVL 树的“紧箍咒”:定义与性质)
-
- [1.1 核心性质](#1.1 核心性质)
- [2. AVL 树的骨架:底层结构实现](#2. AVL 树的骨架:底层结构实现)
- [3. 核心重难点:插入与平衡因子的更新](#3. 核心重难点:插入与平衡因子的更新)
-
- [3.1 平衡因子的更新逻辑(大师级秘籍)](#3.1 平衡因子的更新逻辑(大师级秘籍))
- [补充:AVL 插入中旋转的局部性](#补充:AVL 插入中旋转的局部性)
-
- [1. 什么是"局部性"?](#1. 什么是“局部性”?)
- [2. 核心基石:高度的"借"与"还"](#2. 核心基石:高度的“借”与“还”)
- [3. 单旋与双旋:殊途同归的"高度还原"](#3. 单旋与双旋:殊途同归的“高度还原”)
- [4. 为什么对比之下,删除操作没有绝对的局部性?](#4. 为什么对比之下,删除操作没有绝对的局部性?)
- 总结
- 补充:bf的更新
- [4. 四大旋转:化腐朽为神奇](#4. 四大旋转:化腐朽为神奇)
-
- [招式 1:左单旋 (Left Rotation)](#招式 1:左单旋 (Left Rotation))
- [招式 2:右单旋 (Right Rotation)](#招式 2:右单旋 (Right Rotation))
- [招式 3:右左双旋 (RL Rotation)](#招式 3:右左双旋 (RL Rotation))
- [招式 4:左右双旋 (LR Rotation)](#招式 4:左右双旋 (LR Rotation))
- [5. 如何验证你的 AVL 树是否正确?](#5. 如何验证你的 AVL 树是否正确?)
-
- [补充:AVL 树的查找](#补充:AVL 树的查找)
- [6. 给小白的避坑指南](#6. 给小白的避坑指南)
-
- [祖先节点还没来得及更新 BF 呢,我这层一旋转,难道上面就自动平衡了?](#祖先节点还没来得及更新 BF 呢,我这层一旋转,难道上面就自动平衡了?)
-
- [1. 核心逻辑:高度的"抵消"](#1. 核心逻辑:高度的“抵消”)
- [2. 为什么 Insert 里的 break 如此果断?](#2. 为什么 Insert 里的 break 如此果断?)
- [3. 一个极端的例子](#3. 一个极端的例子)
- 小结
- [7. 完整版代码](#7. 完整版代码)
-
- [AVL 树全逻辑详细注释版](#AVL 树全逻辑详细注释版)
- [💡 核心逻辑画龙点睛](#💡 核心逻辑画龙点睛)
- [8. 总结:AVL 树的工程意义](#8. 总结:AVL 树的工程意义)
- 第二章:红黑树------数据结构的珠穆朗玛峰
-
- [2.1 红黑树的底层逻辑](#2.1 红黑树的底层逻辑)
- [2.2 红黑树的"五条军规" (Properties)](#2.2 红黑树的“五条军规” (Properties))
- [2.3 数据结构定义](#2.3 数据结构定义)
-
- [💡 深度思考:为什么新节点默认是红色?](#💡 深度思考:为什么新节点默认是红色?)
- [2.4 插入操作 (Insert) ------ 核心实战](#2.4 插入操作 (Insert) —— 核心实战)
-
- 变量约定
- [场景 1:叔叔存在且为红 (Uncle is Red) ------ 仅变色](#场景 1:叔叔存在且为红 (Uncle is Red) —— 仅变色)
- [场景 2:旋转 + 变色 (Uncle 不存在 或 Uncle 为黑)](#场景 2:旋转 + 变色 (Uncle 不存在 或 Uncle 为黑))
-
- [💡 核心推导:在本套教学推导中,Uncle 状态与 Cur 来源的关系](#💡 核心推导:在本套教学推导中,Uncle 状态与 Cur 来源的关系)
- [情况 A:Uncle 不存在](#情况 A:Uncle 不存在)
- [情况 B:Uncle 存在且为黑](#情况 B:Uncle 存在且为黑)
- [2.4.3 最终总结](#2.4.3 最终总结)
- [2.5 代码实现](#2.5 代码实现)
- [2.6 如何验证你的红黑树是对的?](#2.6 如何验证你的红黑树是对的?)
- [2.7 完整代码展示](#2.7 完整代码展示)
- [2.8 总结](#2.8 总结)
- 第三章:红黑树的封装------泛型编程的艺术
-
- [3.1 改造节点结构:从 `<K, V>` 到 `<T>`](#3.1 改造节点结构:从
<K, V>到<T>) - [3.2 核心难点:如何比较大小?(KeyOfT)](#3.2 核心难点:如何比较大小?(KeyOfT))
-
- [RBTree 类的改造](#RBTree 类的改造)
- [3.3 迭代器 (Iterator) 的实现](#3.3 迭代器 (Iterator) 的实现)
- [3.4 RBTree.h (核心底层)](#3.4 RBTree.h (核心底层))
- [3.5 封装 MySet](#3.5 封装 MySet)
- [3.6 封装 MyMap](#3.6 封装 MyMap)
- [3.7 总结与思考](#3.7 总结与思考)
- [3.1 改造节点结构:从 `<K, V>` 到 `<T>`](#3.1 改造节点结构:从
第一章:平衡的艺术------从零实现 AVL 树
在二叉搜索树(BST)的学习中,我们留下了一个巨大的隐患:如果数据是有序插入的,树会退化成单链表。为了解决这个"看天吃饭"的缺陷,两位苏联数学家 G.M. Adelson-Velsky 和 E.M. Landis 在 1962 年的论文 《An algorithm for the organization of information》 中提出了一种高度平衡 的搜索树,并以两人的名字命名为 AVL 树。
1. AVL 树的"紧箍咒":定义与性质
AVL 树之所以能保持平衡,是因为它给每个节点都套上了一个"高度差"的限制。
1.1 核心性质
- 搜索树性质 :依然满足 左子树 < 根 < 右子树。
- 高度限制 :左右子树本身也必须是 AVL 树,并且左右子树的高度差(绝对值)不超过 1 。因此,AVL 树可以递归定义为:空树是 AVL 树;非空树在满足二叉搜索树性质的同时,其左右子树都是 AVL 树,且高度差绝对值不超过 1。
- 平衡因子 (Balance Factor) :为了实现方便,我们给每个节点引入一个 BF 变量。(本文采用 平衡因子 = 右子树高度 - 左子树高度;采用相反定义也可以,但整套更新与旋转判断必须保持一致。)
- BF = 0:左右高度相等。
- BF = 1:右子树比左子树高 1 层。
- BF = -1:左子树比右子树高 1 层。
- BF = 2 / -2 :在插入回溯调整的临时状态 中表示该节点已经失衡,必须旋转调整;合法 AVL 树中任一节点最终的 BF 只能是
-1 / 0 / 1。
为什么高度差不是强制等于 0? 因为并非所有节点数量都能组成左右等高的二叉树。例如只有 2 个节点或 4 个节点时,不可能让所有节点的左右子树高度差都为 0,因此 AVL 选择"绝对值不超过 1"作为可实现且足够严格的平衡条件。
由此可将
AVL 树高度控制在 O(log N),所以查找、插入等沿树高进行的操作可以保持在 O(log N) 量级。

2. AVL 树的骨架:底层结构实现
为了能方便地"向上回溯"更新平衡因子,本实现采用三叉链 (左、右、父)。_parent 并不是 AVL 树理论定义所强制要求的;也可以通过递归返回、显式栈等方式完成向上回溯,但三叉链在本套实现中最直观。
cpp
template<class K, class V>
struct AVLTreeNode {
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent; // 关键:指向父亲,方便回溯
int _bf; // balance factor = 右子树高度 - 左子树高度
AVLTreeNode(const pair<K, V>& kv)//创建新节点
: _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0)
{}
};
用于:
Node* newNode = new Node(make_pair(1, "apple")); // 逻辑:数据 -> 节点
3. 核心重难点:插入与平衡因子的更新
插入一个节点分三步:
1.BST 插入: 按大小找到位置,链接新节点(红黑树也是这一步)。
2.更新平衡因子: 这是本 AVL 实现维护平衡的关键步骤,沿着 _parent 指针向上修改受影响祖先的 BF。
3.旋转 (Rotate): 一旦发现 BF 失控,立即执行旋转。
3.1 平衡因子的更新逻辑(大师级秘籍)
当你插入一个新节点时,可能影响从其父节点到根路径上的部分甚至全部祖先的高度;一旦遇到"子树高度不再变化"或完成失衡调整,就可以提前停止:
- 插入在父节点的右边 ,父节点
_bf++; - 插入在父节点的左边 ,父节点
_bf--;
更新后如何处理?
- 情况 A:
parent->_bf == 0- 说明插入前该节点 BF 为 1 或 -1,插入后变平衡了。
- 高度没变,对parent的上一层无影响,停止更新。

-
情况 B:
parent->_bf == 1或-1- 说明插入前该节点 BF 为 0,插入后一侧长高了。
- 高度变了 ,必须继续向上回溯更新
parent->_parent。

-
情况 C:
parent->_bf == 2或-2- 严重失衡! 必须进行旋转,旋转后该局部子树高度会恢复到插入前水平,因此停止继续更新祖先 BF。
- 一次插入 操作,最多只需要进行一次平衡调整 :若是 LL/RR 型,只做 1 次基础旋转;若是 LR/RL 型,则做 2 次基础旋转组成一次双旋。调整完成后,这棵局部子树的高度恢复到插入前,因此高度增长不再继续向上传导。

补充:AVL 插入中旋转的局部性
这里讨论的 "旋转的局部性" (Locality of Rotation) ,应当严格限定在AVL 树插入导致失衡后的旋转修复 这一语境中:当首次失衡节点完成单旋或双旋后,该局部子树的高度恢复到插入前,因此不需要继续向上调整祖先的 BF。红黑树虽然也使用旋转,但其插入修复还包含可能向上传播的变色过程,不能直接套用这里的"高度恢复即停止"结论。
我们来掰开揉碎了,详细拆解这个概念。
1. 什么是"局部性"?
在计算机科学中,"局部性"通常意味着 "影响范围受限" 。
对于 AVL 树的插入操作 而言,所谓"旋转的局部性"是指:当插入一个新节点使某个祖先首次出现 BF = ±2 时,只需要对这个最小失衡子树做常数级的指针调整(单旋或双旋),就能让该局部子树恢复平衡,并使其总高度恢复到插入前的水平,因此更高层祖先不需要再因为这次插入继续更新 BF。
2. 核心基石:高度的"借"与"还"
要彻底搞懂局部性,我们必须盯紧一个核心指标:子树的总高度 ( H H H)。
假设我们有一棵庞大的平衡树,其中有一个局部子树,其根节点为 A。在插入新节点之前 ,以 A 为根的子树高度为 h h h。
- 第一步:打破平衡(高度 +1)
我们在A的某条极深的路径上插入了一个新节点。这个新节点导致那条路径变长了。如果这个变长恰好让A节点的左右子树高度差达到了 2,A就成了最小失衡树的根节点 。
此时,在尚未进行旋转的这一瞬间,以A为根的子树,其总高度暂时 变成了 h + 1 h + 1 h+1。 - 第二步:旋转修复(高度 -1)
这是最关键的一步。无论是 LL、RR 单旋,还是 LR、RL 双旋,它们在空间几何上做的本质动作是:把树形压扁 。
旋转操作会将原本较深的那一侧的节点"提拉"上来成为新的局部根节点,而把原本的根节点A"下放"成为子节点。
经过这样的"压扁"操作,这棵局部子树(由失衡时的全部原节点所组成的子树)的总高度又从 h + 1 h + 1 h+1 退回到了 h h h。 - 第三步:局部性达成(停止传导)
现在,请站在A节点的父节点 (甚至是整棵大树的根节点)的视角来看:
在插入节点前,我下面的这棵子树高度是 h h h;插入后短暂的变为h+1 ,但插入并旋转完成后,这棵子树的高度依然是 h h h 。
既然高度没变,那么父节点的平衡因子(BF)就不会改变,祖父节点也不会改变......平衡的破坏被完美地"消化"在了这个局部范围内,失衡信号停止向上传导。
3. 单旋与双旋:殊途同归的"高度还原"
为了保证"局部性"绝对成立,仅仅有单旋是不够的,这也是为什么我们需要双旋(LR, RL)。
- 单旋 (LL / RR) 的局部性:
当新节点像一根直棍子一样插入在外侧(比如左孩子的左子树),我们只需要一次右旋(LL 型)。旋转后,原失衡根与其高侧孩子的上下级关系发生反转:高侧孩子成为新的局部根,原失衡根下沉为其子节点,同时中间子树重新挂接;中序次序保持不变,高度恢复到插入前。 - 双旋 (LR / RL) 的局部性:
当新节点插入在内侧(比如左孩子的右子树),树形像一个"拐点"或"膝盖"。如果仅仅做一次单旋,你会发现树的高度并没有降下来 ,失衡依然存在。
这就是"双旋"发挥作用的地方:先对子节点做一次局部旋转(把"弯"的树形捋直),然后再对根节点做一次反向旋转。两次旋转组合在一起,其最终的数学结果依然是:子树总高度减 1,完美恢复到插入前的 h h h。
无论是哪种情况,算法的终极目标都是在局部找回那个失去的 h h h。
4. 为什么对比之下,删除操作没有绝对的局部性?
理解了插入的局部性,反过来想删除操作,逻辑就更加通透了。
当你在 AVL 树中删除一个节点时:
- 删除节点导致某条路径变短。
- 为了修复失衡,你进行旋转。
- 旋转虽然让当前子树平衡了,但是!旋转后的子树总高度,可能比删除前矮了 1 层。
- 一旦子树变矮,对于父节点来说,它的左右子树高度差就改变了,父节点可能因此失衡。
- 这就引发了"连锁反应",失衡信号随着高度的降低一路向上传导,导致最坏情况下需要一路旋转到根节点(最多 O(log n)次旋转)。
总结
"旋转的局部性"是算法工程师与数学达成的一次精妙妥协:我们允许局部因为插入而短暂失衡,但通过巧妙的 O ( 1 ) O(1) O(1) 空间几何变换,将由于插入而膨胀的"高度"在局部强行压缩回去。这就是平衡树之所以"平衡"且"高效"的核心秘诀。
补充:bf的更新
插入 AVL:
1. LL/RR 单旋只需将原根 + 新根,两个 BF 全部归 0。
2. LR/RL 双旋中拐点必归 0,拐点原来为 0 则三个全 0;如果拐点原 BF=±1,那么另外两个旧节点中:一个为 0,另一个为非 0;这个非 0 的 BF 一定与拐点旋转前的 BF 符号相反,具体谁取非零,就看拐点较矮的子树旋转后分配给了谁。
4. 四大旋转:化腐朽为神奇
这是 AVL 树最核心的部分,旨在通过改变链接关系,在不改变中序遍历序列的前提下,压低树的高度。
"找到底部第一个失衡节点,拎出它往下的两层路径节点(共祖孙三个),把这三大节点的指针重新接好,一次旋转搞定全局平衡。"
招式 1:左单旋 (Left Rotation)
触发条件 :右边高,且新节点插在右孩子的右边(RR型)。
口诀:右孩子做新根,原根变左子,右孩子的左子变原根的右子。

cpp
// 左单旋:用于解决 RR 型失衡(插入的节点位于失衡节点的右子树的右侧)
// 这里的 parent 就是底部第一个失衡节点(即我们之前说的"爷爷" z)
void RotateL(Node* parent)
{
// ========== 1. 获取关键的三层节点(祖孙三代) ==========
// subR (y):失衡节点 parent(z)的右孩子,即路径上的"子节点"
// 旋转完成后,subR 将晋升为这棵子树的"新根"
Node* subR = parent->_right;
// subRL (y的左子树):它属于"孙辈"节点(x)的左子树(或者说,是介于 parent 和 subR 之间的中间子树)
// 它的值比 parent 大,但比 subR 小,旋转时需要"过继"给 parent
Node* subRL = subR->_left;
// ========== 2. 第一步:将"孙辈的左子树"过继给"爷爷" ==========
// 将 subRL 挂接到 parent 的右子树位置(因为 subRL 的所有节点都大于 parent)
// 此时,subRL 成为 parent(z)的右孩子
parent->_right = subRL;
// 如果 subRL 非空,更新其父指针指向 parent(认爷爷做父)
if (subRL)
subRL->_parent = parent;
// ========== 3. 第二步:保存"爷爷"的父节点,准备让"子节点"上位 ==========
// pParent:记录原 parent(z)的父节点,用于后续将旋转后的子树接回整棵树
Node* pParent = parent->_parent;
// ========== 4. 第三步:核心旋转动作,让"子节点(y)"晋升为"新根" ==========
// subR(y)的左孩子指向 parent(z),即让 z 降级成为 y 的左孩子
subR->_left = parent;
// 更新 parent(z)的父指针,现在它的父亲变成了原本的"儿子" subR
parent->_parent = subR;
// ========== 5. 第四步:将调整好的新子树(以 subR 为根),接回整棵大树 ==========
// 判断原 parent 是不是整棵树的根节点
if (pParent == nullptr)
{
// 如果 pParent 为空,说明原 parent 是整棵树的根节点
// 现在新根变成 subR(y),将其设为树的 _root,父指针置空
_root = subR;
subR->_parent = nullptr;
}
else
{
// 如果 pParent 非空,需要判断原 parent 是 pParent 的左孩子还是右孩子
// 用 subR(新根)替换掉原来 parent(z)的位置
if (pParent->_left == parent)
pParent->_left = subR; // 原 parent 是左子树,则新根接在左侧
else
pParent->_right = subR; // 原 parent 是右子树,则新根接在右侧
// 更新新根 subR 的父指针,指向原来的祖父节点 pParent
subR->_parent = pParent;
}
// ========== 6. 第五步:更新平衡因子 ==========
// 旋转结束后,以 parent(原 z)和 subR(原 y)为根的子树高度完全一致
// 因此它们的平衡因子都恢复为 0
parent->_bf = 0;
subR->_bf = 0;
}
招式 2:右单旋 (Right Rotation)
触发条件 :左边高,且插在左孩子的左边(LL型)。
逻辑 :与左单旋完全镜像对称。

招式 3:右左双旋 (RL Rotation)
触发条件 :右边高,但插在右孩子的左边(RL型)。
逻辑:先对右孩子做右单旋,再对父节点做左单旋。
三种情况:双旋"拐点"节点的 BF 可能为 -1 / 0 / 1,旋转后的 BF 更新不同
以 RotateRL(parent) 为例,设 subR = parent->_right、subRL = subR->_left,并在旋转前保存 bf = subRL->_bf:
bf |
parent->_bf |
subR->_bf |
subRL->_bf |
|---|---|---|---|
0 |
0 |
0 |
0 |
1 |
-1 |
0 |
0 |
-1 |
0 |
1 |
0 |
这也是为什么双旋不能像插入场景下的单旋那样简单地把所有相关 BF 都直接设为 0。

招式 4:左右双旋 (LR Rotation)
触发条件 :左边高,但插在左孩子的右边(针对下图 90 节点而言,即 LR 型,折线形)。
逻辑 :先对左孩子做左单旋,再对父节点做右单旋。
难点 :旋转后的 BF 更新取决于"拐点"节点(subLR)旋转前的 BF。
以 RotateLR(parent) 为例,设 subL = parent->_left、subLR = subL->_right,并在旋转前保存 bf = subLR->_bf:
bf |
parent->_bf |
subL->_bf |
subLR->_bf |
|---|---|---|---|
0 |
0 |
0 |
0 |
-1 |
1 |
0 |
0 |
1 |
0 |
-1 |
0 |

5. 如何验证你的 AVL 树是否正确?
作为一个底层大师,不能只写代码,还要会写测试。
- 验证搜索性质:中序遍历是否有序。
- 验证平衡性质:递归求每个节点的左右高度差,看绝对值是否超过 1。
- 验证记录的 BF 是否正确 :重新计算
rightHeight - leftHeight,检查它是否与节点中保存的_bf一致。空树本身也应判定为合法 AVL 树。
cpp
int _Height(Node* root) {
if (root == nullptr) return 0;
return max(_Height(root->_left), _Height(root->_right)) + 1;
}
bool _IsBalance(Node* root) {
if (root == nullptr) return true;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
if (rightH - leftH != root->_bf) {
cout << root->_kv.first << "节点平衡因子异常" << endl;
return false;
}
return abs(rightH - leftH) < 2 && _IsBalance(root->_left) && _IsBalance(root->_right);
}
补充:AVL 树的查找
AVL 树仍然是一棵二叉搜索树,因此查找逻辑与普通 BST 相同:目标 key 小于当前节点就走左子树,大于当前节点就走右子树,相等则找到。区别不在"查找代码本身",而在于 AVL 通过平衡约束把树高稳定在 O ( log N ) O(\log N) O(logN),因此查找最坏复杂度为 O ( log N ) O(\log N) O(logN)。完整版代码中的 Find 正是这一逻辑。
6. 给小白的避坑指南
- 三叉链的
_parent链接 :每次旋转不仅要改_left和_right,必须同步更新_parent。这是新手最容易漏掉的地方,会导致Insert回溯时死循环。 - 旋转后的
break: 一旦旋转完成,这棵子树的高度就回到了插入前的状态,它对祖先不再产生高度影响,所以一定要 break,否则会过度调整。 - 空指针判断 :在单旋中,
subRL或subLR可能为空(当树比较简单时),在访问其_parent前必须判空。
祖先节点还没来得及更新 BF 呢,我这层一旋转,难道上面就自动平衡了?
答案是:是的,因为祖先节点根本"感觉不到"下面发生了变化。
我们可以把 AVL 树的平衡理解为一种"高度守恒"。
1. 核心逻辑:高度的"抵消"
在 AVL 树中,祖先节点的 BF 是否需要更新,唯一取决于 以 parent 为根的这棵子树的总高度是否发生了变化。
- 插入前 :假设这棵最小失衡子树在插入前的高度是 H H H。
- 插入后(旋转前) :新节点使这棵子树的高度暂时变成 H + 1 H+1 H+1。此时如果不旋转,这 1 层高度增长就会继续影响祖先的 BF。
- 旋转后 :无论是单旋还是双旋,这棵局部子树都会恢复平衡,并将高度重新恢复为 H H H。
结论:
对于祖先节点来说,它看到的下级子树高度在插入前是 H H H,旋转后依然是 H H H。既然高度没变,尚未更新到的更高层祖先就不需要再因这次插入修改 BF,也不会因为这次局部高度增长而新增失衡。
2. 为什么 Insert 里的 break 如此果断?
请看你代码中逻辑的三个出口,这是理解"祖先是否平衡"的关键:
| 情况 | 父节点 BF 更新结果 | 树的高度变化 | 处理动作 | 结果 |
|---|---|---|---|---|
| 出口 1 | parent->_bf == 0 |
高度不变 | break |
插入补齐了矮的一边,祖先不受影响。 |
| 出口 2 | parent->_bf == 1/-1 |
高度 +1 | continue |
必须向上更新,因为这 1 层高度差会传导给祖先。 |
| 出口 3 | parent->_bf == 2/-2 |
旋转前 +1,旋转后恢复 | 旋转并 break |
旋转"消化"了多出的高度,祖先完全察觉不到。 |
3. 一个极端的例子
想象一棵 100 层高的 AVL 树。
- 你在第 100 层插入了一个新节点。
- 第 99 层、98 层、97 层的 BF 以前都是 0,现在都变成了 1。这意味着高度在向上传递。
- 到了第 96 层,BF 变成了 2(失衡)。
- 此时你执行旋转。
- 奇迹发生了:第 96 层这棵子树的高度,通过旋转变回了插入前的高度。
- 对于第 95 层到第 1 层的祖先来说:它们看到的第 96 层子树高度没变,所以它们的 BF 依然维持原样。
这就是为什么 AVL 树的一次插入,最多只需要对第一个失衡祖先做 1 次平衡调整:单旋包含 1 次基础旋转,双旋包含 2 次基础旋转。
小结
在 Insert 中,旋转不是为了"修补"祖先,而是为了"截断"高度的传递。旋转就像是一个高度灭火器,火(高度增加)烧到这一层被扑灭了,上面的建筑(祖先)自然就是安全的。
7. 完整版代码
在模板类内部,可以省略模板参数;但在模板类外部,必须加上模板参数。如果你习惯把函数的声明和定义分离(虽然模板通常写在同一个 .h 里),在类外部实现时,必须显式指定模板参数,即
template
Box<T>::Box() { // 必须写 Box<T>
// 构造函数定义
}
AVL 树全逻辑详细注释版
cpp
#include <iostream>
#include <cassert> // 提供 assert 宏
#include <algorithm> // 提供 swap 和 max
#include <vector> // 用于压力测试
#include <ctime> // 用于随机种子
#include <utility> // pair 类型
using namespace std;
// ======================================================================
// 1. AVL 树节点结构
// ======================================================================
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // 键值对
AVLTreeNode<K, V>* _left; // 左孩子
AVLTreeNode<K, V>* _right; // 右孩子
AVLTreeNode<K, V>* _parent; // 父节点(旋转和回溯必须)
int _bf; // 平衡因子 = 右子树高度 - 左子树高度
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
// ======================================================================
// 2. AVL 树类定义
// ======================================================================
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
// -------------------- 构造 / 析构 / 拷贝 / 赋值 --------------------
AVLTree() = default; // 默认构造,_root 被初始化为 nullptr
// 拷贝构造(深拷贝)
AVLTree(const AVLTree<K, V>& t)
{
_root = Copy(t._root);
}
// 赋值运算符重载(现代写法:传值 + swap)
AVLTree<K, V>& operator=(AVLTree<K, V> t)
{
swap(_root, t._root); // 交换根指针,t 析构时释放旧资源
return *this;
}
// 析构函数
~AVLTree()
{
Destroy(_root);
_root = nullptr;
}
// -------------------- 核心插入操作 --------------------
bool Insert(const pair<K, V>& kv)
{
// 1. 空树直接作为根
if (_root == nullptr)
{
_root = new Node(kv);
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; // Key 已存在,不允许重复
}
}
// 3. 链接新节点
cur = new Node(kv);
if (parent->_kv.first < kv.first)
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// ============================================================
// 4. 更新平衡因子 + 旋转调整(AVL 的精髓)
// 插入节点只影响其祖先的平衡因子,必须从父节点向上回溯
// ============================================================
while (parent) // 一直向上直到根
{
// ---------- 4.1 更新当前父节点的平衡因子 ----------
// 根据 cur 是左孩子还是右孩子,决定 _bf 的变化方向
if (cur == parent->_left)
parent->_bf--; // 左子树增高,平衡因子减 1
else
parent->_bf++; // 右子树增高,平衡因子加 1
// ---------- 4.2 根据更新后的 _bf 值分类处理 ----------
if (parent->_bf == 0)
{
// 情况 A:_bf 从 ±1 变为 0
// 说明原本一边高,现在被新节点填平了,子树高度不变
// 因此不需要继续向上更新,直接跳出循环
break;
}
else if (parent->_bf == 1 || parent->_bf == -1)
{
// 情况 B:_bf 从 0 变为 ±1
// 说明原本平衡,现在变成一边高,子树高度增加了 1
// 需要继续向上通知祖先节点
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
// 情况 C:_bf 变为 ±2,严重失衡,必须旋转
// 此时 parent 就是**第一个失衡节点(底部失衡点)**
// 根据其孩子 cur 的 _bf 判断旋转类型
// ---- C1: 右右型(RR)----
// parent 右高 2,且 cur(右孩子)也右高 1
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); // 左单旋
}
// ---- C2: 左左型(LL)----
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent); // 右单旋
}
// ---- C3: 右左型(RL)----
// parent 右高 2,但 cur(右孩子)左高 1(折线)
else if (parent->_bf == 2 && cur->_bf == -1)
{
RotateRL(parent); // 右左双旋
}
// ---- C4: 左右型(LR)----
// parent 左高 2,cur(左孩子)右高 1(折线)
else // parent->_bf == -2 && cur->_bf == 1
{
RotateLR(parent); // 左右双旋
}
// 旋转后,以 parent 为根的子树高度恢复到插入前的水平
// 因此祖先的平衡因子无需再更新,直接退出循环
break;
}
else
{
// 正常情况下 _bf 不会超出 [-2, 2],若触发则说明程序有 Bug
assert(false);
}
}
return true;
}
// -------------------- 查找 --------------------
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;
}
// -------------------- 遍历与测试接口 --------------------
void InOrder()
{
_InOrder(_root);
cout << endl;
}
int Height()
{
return _Height(_root);
}
bool IsBalance()
{
return _IsBalance(_root);
}
private:
// ---------- 内部辅助函数 ----------
void _InOrder(Node* root)
{
if (!root) return;
_InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << " ";
_InOrder(root->_right);
}
int _Height(Node* root)
{
if (!root) return 0;
return max(_Height(root->_left), _Height(root->_right)) + 1;
}
// 递归检查平衡性(同时校验 _bf 是否与真实高度差一致)
bool _IsBalance(Node* root)
{
if (!root) return true;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
int diff = rightH - leftH;
if (abs(diff) >= 2)
{
cout << "失衡节点: " << root->_kv.first << " 高度差=" << diff << endl;
return false;
}
if (diff != root->_bf)
{
cout << "BF 记录错误: " << root->_kv.first
<< " 实际=" << diff << " 记录=" << root->_bf << endl;
return false;
}
return _IsBalance(root->_left) && _IsBalance(root->_right);
}
// ---------- 旋转实现(带祖孙三代角色说明) ----------
// ==================================================================
// 左单旋(RR 型)
// 场景:parent(z)失衡,右孩子 subR(y)也偏高
// parent->_bf == 2 且 subR->_bf == 1
// 操作:使 subR(y)上升为新根,parent(z)降为其左孩子
// ==================================================================
void RotateL(Node* parent)
{
// 祖孙三代:
// parent ------ 失衡节点(z,爷爷)
// subR ------ 右孩子(y,爸爸)
// subRL ------ subR 的左孩子(属于孙辈节点 x 的左子树)
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1. 将 subRL 过继给 parent 作为右子树
// 因为 subRL 的所有值都大于 parent,小于 subR
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
// 2. 让 parent 成为 subR 的左孩子
Node* parentParent = parent->_parent; // 保存更高层的祖先
subR->_left = parent;
parent->_parent = subR;
// 3. 将 subR 与原祖先连接(subR 成为新的子树根)
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
// 4. 更新平衡因子(旋转后 parent 和 subR 高度一致,均为 0)
parent->_bf = subR->_bf = 0;
}
// ==================================================================
// 右单旋(LL 型)
// 场景:parent 左高,左孩子也左高(对称镜像)
// ==================================================================
void RotateR(Node* parent)
{
// 祖孙:parent(z),subL(y),subLR(孙辈 x 的右子树)
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 1. subLR 过继给 parent 作为左子树
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
// 2. parent 成为 subL 的右孩子
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
// 3. subL 上位为新根
if (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
// 4. 更新平衡因子
parent->_bf = subL->_bf = 0;
}
// ==================================================================
// 右左双旋(RL 型)
// 场景:parent 右高 2,但右孩子 subR 左高 1(折线)
// 先对 subR 进行右旋(使其变成 RR 型),再对 parent 左旋
// ==================================================================
void RotateRL(Node* parent)
{
// 关键节点:
// parent ------ 失衡点(z)
// subR ------ parent 的右孩子(y)
// subRL ------ subR 的左孩子(孙辈 x),它的平衡因子决定最终校正值
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf; // 记录旋转前 subRL 的 BF
// 1. 对 subR 右旋(将折线拉直)
RotateR(parent->_right);
// 2. 对 parent 左旋(最终平衡)
RotateL(parent);
// 3. 修正平衡因子(根据 subRL 原先的情况分类)
// 因为双旋后,parent、subR、subRL 的 BF 都需要重新设置
if (bf == 0)
{
// 如果 subRL 本身是新增节点(或高度为 0),则三节点都平衡
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else if (bf == 1)
{
// 新节点插在 subRL 的右边(右子树高)
// 此时 parent 分到了较矮的左子树,故 BF = -1
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == -1)
{
// 新节点插在 subRL 的左边(左子树高)
// 此时 subR 分到了较矮的右子树,故 BF = 1
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
// ==================================================================
// 左右双旋(LR 型)
// 场景:parent 左高 2,左孩子 subL 右高 1(镜像)
// 先对 subL 左旋,再对 parent 右旋
// ==================================================================
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 记录旋转前 subLR 的 BF
// 1. 对 subL 左旋
RotateL(parent->_left);
// 2. 对 parent 右旋
RotateR(parent);
// 3. 修正平衡因子(与 RL 对称)
if (bf == 0)
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
}
else if (bf == -1) // 新节点插在 subLR 的左边
{
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1; // parent 分到了较矮的右子树
}
else if (bf == 1) // 新节点插在 subLR 的右边
{
subL->_bf = -1; // subL 分到了较矮的左子树
subLR->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
// ---------- 销毁与拷贝 ----------
void Destroy(Node* root)
{
if (!root) return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
// 深拷贝:递归复制节点,并正确重建父指针
Node* Copy(Node* root)
{
if (!root) return nullptr;
Node* newRoot = new Node(root->_kv);
newRoot->_bf = root->_bf;
newRoot->_left = Copy(root->_left);
newRoot->_right = Copy(root->_right);
// 重建父指针(关键!)
if (newRoot->_left)
newRoot->_left->_parent = newRoot;
if (newRoot->_right)
newRoot->_right->_parent = newRoot;
return newRoot;
}
private:
Node* _root = nullptr;
};
// ======================================================================
// 3. 测试代码
// ======================================================================
void TestAVLTree()
{
AVLTree<int, int> t;
int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
for (auto e : a)
{
t.Insert({e, e});
}
t.InOrder(); // 输出升序
cout << "高度: " << t.Height() << endl;
cout << "是否平衡: " << (t.IsBalance() ? "是" : "否") << endl;
}
void TestAVLTree_Advanced()
{
srand((unsigned)time(0));
const int N = 10000;
vector<int> v;
v.reserve(N);
for (int i = 0; i < N; ++i)
v.push_back(rand() + i);
AVLTree<int, int> t;
for (auto e : v)
t.Insert({e, e});
cout << "\n--- 高压测试 ---\n";
cout << "插入 " << N << " 个随机数\n";
cout << "树高: " << t.Height() << endl;
cout << "平衡性: " << (t.IsBalance() ? "通过" : "失败") << endl;
}
int main()
{
TestAVLTree();
// TestAVLTree_Advanced(); // 默认注释,可取消以进行压力测试
return 0;
}
💡 核心逻辑画龙点睛
- 三叉链的"爱恨情仇" :在单旋逻辑中(如
RotateL),我详细注释了三个核心步骤。最容易错的是第三步------处理与上层建筑的连接 。如果parent原本是整棵树的根,旋转后你必须更新_root;如果它只是个子树,你必须更新它爷爷的孩子指针。 - 双旋的"谍战博弈" :双旋后谁成了新根?是原本那个处于转折点位置的节点。在
RotateRL中,subRL最终被提拔到了最高位,它原本携带的孩子分别分给了原来的爸爸和原来的爷爷,所以需要根据bf的值(即它的孩子原本在左还是在右)来手动校正。 assert(false)的意义:作为底层大师,这是你的防线。如果在回溯更新中 BF 超出了 -2, 2 的范围,说明逻辑在之前的步骤就出错了,必须立刻定位问题。
8. 总结:AVL 树的工程意义
- 时间复杂度 :由于 AVL 树高度为 O ( log N ) O(\log N) O(logN),查找、插入、删除的最坏时间复杂度均为 O ( log N ) O(\log N) O(logN)。
- 代价 :AVL 对高度平衡控制更严格。插入 最多只需要对第一个失衡点做一次平衡调整(单旋或双旋);删除 则可能因为子树高度持续降低而向上级联调整,最坏可发生 O ( log N ) O(\log N) O(logN) 次旋转。
- 现状 :在许多通用有序关联容器实现中,更常见的是平衡条件稍宽松、更新时旋转通常更少的红黑树 。例如 SGI STL 的
map/set复用红黑树;但 C++ 标准本身只规定容器语义和复杂度,并不强制std::map/std::set必须使用红黑树。AVL 依然是学习平衡搜索树的最佳练兵场。
第二章:红黑树------数据结构的珠穆朗玛峰
在上一篇中,我们手撕了二叉搜索树(BST)。BST 的查找复杂度本质上取决于树高 h h h,即 O ( h ) O(h) O(h):在较平衡时可达到 O ( log N ) O(\log N) O(logN),但在有序或接近有序的数据插入下可能退化成单链表,此时树高变为 N N N,查找最坏退化至 O ( N ) O(N) O(N)。
为了解决这个问题,Adelson-Velsky 和 Landis 提出了 AVL 树 (高度平衡二叉搜索树)。它对左右子树高度差控制得非常严格(绝对值不超过 1):插入时最多在第一个失衡点做一次平衡调整,但删除时可能出现向上的级联调整,因此相较红黑树通常需要维护更严格的平衡信息。
于是,红黑树 (Red-Black Tree) 应运而生。它是一种通过颜色约束实现近似平衡的二叉搜索树 。许多 C++ 标准库实现中的 std::map 和 std::set 采用红黑树作为底层结构; 不过C++ 标准并未强制规定必须使用红黑树。
2.1 红黑树的底层逻辑
红黑树不追求"绝对高度平衡",而是追求 "大致平衡" 。它通过一套颜色规则,保证了以下核心定律:
核心法则: 通过颜色约束,确保从根到各个 NIL/NULL 叶子位置 的路径中,最长路径不会超过最短路径的 2 倍。
由于从同一节点到各个 NIL 的路径具有相同的黑高,并且红节点不能连续出现,所以在极端情况下,较短路径可以近似看作"全黑",较长路径可以近似看作"红黑交替"。因此,红节点数量不会超过同一路径上的黑节点数量,最长路径至多约为最短路径的 2 倍。
这意味着红黑树的高度依然保持在 O ( log N ) O(\log N) O(logN) 量级,因此查找、插入、删除的最坏时间复杂度都可以保持在 O ( log N ) O(\log N) O(logN)。与 AVL 相比,红黑树对平衡的要求更宽松,通常能够减少更新过程中的旋转次数。
从黑高角度可以进一步理解:若某条最短根到 NIL 路径包含约 bh 个黑节点,那么因为不能有连续红节点,最长路径至多约为 2 * bh;而一棵包含 N N N 个内部节点的红黑树高度仍被约束在对数量级。
2.2 红黑树的"五条军规" (Properties)
实现红黑树之前,必须理解这 5 条性质。后续所有的代码逻辑(变色、旋转),全是为了维护这些规则不被破坏。
- 节点颜色 :每个节点不是 红色 就是 黑色。
- 根节点 :根节点必须是 黑色。
- 叶子黑 :所有的 NIL 节点(即空指针
nullptr)被视为 黑色。(用于计算路径) - 不红红 :如果一个节点是 红色,则它的两个子节点必须是 黑色 。(这意味着路径上不能出现连续的红色节点)。
- 黑高同 :从任一节点到其每个叶子的所有路径,都包含相同数目 的 黑色 节点。
2.3 数据结构定义
为了方便后续实现 map 和 set 的迭代器,我们的节点结构采用三叉链(包含指向父节点的指针)。
cpp
enum Colour {
RED,
BLACK
};
template<class T>
struct RBTreeNode {
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent; // 指向父节点,方便回溯
T _data;
Colour _col;
RBTreeNode(const T& data)
: _left(nullptr), _right(nullptr), _parent(nullptr)
, _data(data)
, _col(RED) // 重点:新插入的节点默认是红色
{}
};
💡 深度思考:为什么新节点默认是红色?
- 如果在非空树中把普通新节点插成黑色 :会使所有经过该新节点的路径黑高 +1,而其他路径不变,从而直接破坏性质 5(黑高同)。空树插入第一个根节点是特例,根最终必须为黑色。
- 如果插入红色 :可能会破坏性质 4(不红红)。但这只在"父节点也是红色"时才会发生。如果父节点是黑的,插入红色完全没问题。调整连续红节点的代价通常比调整全局黑高要小。
2.4 插入操作 (Insert) ------ 核心实战
红黑树的插入过程分为两步:
- BST 插入:按照二叉搜索树的规则,找到位置并插入新节点(红色)。
- 自平衡调整 :如果插入后,父节点也是红色,违反了"不红红"规则,需要通过变色 或旋转来修复。
(来源见水印,以下图片均来源于水印处)

变量约定
为了方便描述,我们约定:
cur:当前节点(新插入的,或向上调整时的当前节点)。p(Parent):父节点。g(Grandfather):祖父节点。u(Uncle):叔叔节点(父节点的兄弟)。
调整的核心也就是看叔叔 u 的脸色。 所有的讨论基于 p 是红色(需要调整)这一前提。
在这场博弈中,祖父节点 (g) 一定是黑色的 (否则插入前就已非法),父节点 (p) 一定是红色的 (否则无需调整)。
因此,决定战术的关键变量,在于叔叔节点 (u) 的状态。
我们分两种核心场景讨论:
场景 1:叔叔存在且为红 (Uncle is Red) ------ 仅变色

情况描述 :
cur 为红,parent 为红,uncle 存在且也为红色。
操作逻辑:
- 将
parent和uncle变为 黑色。 - 将
grandfather变为 红色。
原理解析 :
这样变色后,经过 parent 一侧和经过 uncle 一侧的路径都各自增加 1 个黑节点 ;同时把原本为黑的 grandfather 变红,又减少 1 个黑节点,因此从 grandfather 上方看,这棵子树的黑高整体保持不变。
潜在问题(连锁反应) :
虽然局部平衡解决了,但 grandfather 变红了。
- 如果
grandfather是根节点,直接把它再染黑即可。 - 如果
grandfather的父节点也是红色,那么变色后会再次出现连续的红色节点。
应对策略 :
我们需要继续向上调整 。将 grandfather 当作新的 cur,重新找到新的 parent 和 uncle,继续循环判断是变色还是旋转,直到遇到根节点或者不再出现连续红节点为止。
总结 :在当前
parent已经为红、需要修复连续红的前提下,只要uncle也为红,策略就是 "变色 + 向上继续检查" 。局部不需要旋转,但grandfather变红后可能与更高层形成新的连续红。
场景 2:旋转 + 变色 (Uncle 不存在 或 Uncle 为黑)
当 uncle 不是红色时,单纯的变色已经无法解决问题,必须引入旋转 来改变树的拓扑结构。在分类讨论之前,我们需要先厘清一个极具深度的逻辑:cur 节点到底是谁?
💡 核心推导:在本套教学推导中,Uncle 状态与 Cur 来源的关系
在这套插入实现的循环中,一个节点被标记为 cur,有两种来源:要么它是刚 new 出来的新增节点 ,要么它是场景 1 变色后将 grandfather 上移得到的继续调整节点 。下面关于 uncle 与 cur 来源的推导,是针对该实现和图示中的黑高层次关系而言。
-
当 Uncle 不存在时 Cur 一定是新增节点
- 原因 :如果
cur不是新增节点,而是由下方变色上来的,那意味着cur下面的路径上至少包含了一个黑色节点(黑高至少为1)。而uncle不存在,意味着从grandfather向uncle走是直接到NULL,黑高为 0。这导致左右黑高不一致,说明在插入前这棵树就已经不是红黑树了。所以,uncle不存在时,cur只能是刚插入的那个新节点。
- 原因 :如果
-
当 Uncle 为黑色时 Cur 一定是向上调整来的节点
- 原因 :
uncle是黑色,说明从grandfather到uncle的路径上至少有 1 个黑节点。如果cur是新增节点(原本路径没黑节点),那么插入位置的路径黑高比uncle这边少,这违反了性质 5。因此,cur所在的子树原本一定包含黑色节点,只是因为下层的"场景1"变色,导致cur变成了红色。
- 原因 :
理清了这个逻辑,我们将场景 2 细分为两种情况讨论:
情况 A:Uncle 不存在
由推导可知,此时 cur 是新增节点 。

为什么不能仅变色?
如果直接把 parent 变黑、grandfather 变红,那么 parent 这边的黑高变成了 1(有一个黑节点),而 uncle (NULL) 那边的黑高依然是 0。黑高失衡,违反性质 5。
解决方案:旋转 + 变色
根据三个节点(grandfather, parent, cur)的相对位置,分单旋和双旋:
- 单旋 + 变色(直线型)
- 形态 :
cur是parent的左孩子,parent是grandfather的左孩子(LL型);或者对称的 RR 型。 - 操作 :以
grandfather为轴进行旋转(LL做右旋)。然后Parent 变黑,Grandfather 变红。

- 双旋 + 变色(折线型)
- 形态 :
cur是parent的右孩子,parent是grandfather的左孩子(LR型);或者对称的 RL 型。 - 操作 :先以
parent为轴旋转(左旋),变成直线型;再以grandfather为轴旋转(右旋)。最后Cur 变黑,Grandfather 变红。
结果 :旋转变色后,该子树的根变成了黑色,且黑高恢复正常。无需继续向上调整,插入结束。

情况 B:Uncle 存在且为黑
注意:在插入修复的分支选择 上,"Uncle 不存在(
nullptr,按黑色 NIL 处理)"和"Uncle 存在且为黑色"都进入 旋转 + 变色 这一大类,因此主体处理逻辑可以复用;但二者在上面的cur来源推导中仍有不同背景。
由推导可知,此时 cur 是向上调整来的节点(原本是黑的,被下层变色染红了)。

虽然 cur 的来源不同,但处理逻辑与"Uncle 不存在"的情况完全一致:
- 直线型 (LL/RR) :单旋 。操作完成后,Parent 变黑,Grandfather 变红。
- 折线型 (LR/RL) :双旋 。操作完成后,Cur 变黑,Grandfather 变红。
结果 :该子树的新根节点变为黑色,阻断了红色的向上传播,无需继续向上调整,插入结束。
2.4.3 最终总结
红黑树的插入调整逻辑可以用以下流程图概括:
1. 看 Uncle 的颜色 。
2. Uncle 为红 变色 (p, u 变黑, g 变红) 向上调整 (Continue)。
3. Uncle 为黑 / 不存在 旋转 + 变色 (单旋或双旋) 停止调整 (Break)。
4. 注意:无论经过何种调整,最后一定要将整棵树的根节点强制设为黑色。
2.5 代码实现
以下是红黑树插入操作的精简核心代码。
cpp
// ----------------------------------------------------------------------
// 红黑树核心插入函数
// 性质回顾(辅助理解代码):
// 1. 每个节点要么红,要么黑
// 2. 根节点必须是黑
// 3. 所有 NIL 叶子节点视为黑
// 4. 如果一个节点是红的,它的两个孩子必须是黑的(不能有连续红节点)
// 5. 从任一节点到其每个叶子的所有路径,包含相同数量的黑节点(黑高一致)
// ----------------------------------------------------------------------
bool Insert(const T& data) {
// --- 1. 处理空树情况 ---
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK; // 性质2:根节点必须是黑色
return true;
}
// --- 2. 标准 BST 搜索插入位置 ---
Node* parent = nullptr; // 记录 cur 的父节点
Node* cur = _root;
while (cur) {
parent = cur;
if (data < cur->_data)
cur = cur->_left; // 比当前小,往左走
else if (data > cur->_data)
cur = cur->_right; // 比当前大,往右走
else
return false; // 键值已存在,插入失败(红黑树/AVL树通常不允许冗余)
}
// --- 3. 链接新节点 ---
cur = new Node(data);
cur->_col = RED; // 【关键策略】新节点默认染红色
// 原因:染红色不会违反性质5(黑高不变),只会可能违反性质4(连续红)
// 如果染黑色,会导致所有经过该节点的路径黑高+1,性质5直接崩溃,修复难度极大
if (data < parent->_data)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent; // 三叉链:别忘了维护父指针
// --- 4. 红黑树性质修复(核心逻辑) ---
// 循环条件:只要父节点存在且是红色,就违反了性质4(红父不能有红子),必须继续调整
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent; // 祖父节点一定存在(因为父是红,性质4要求父的父必须是黑)
// =============================================================
// 【左半区】父节点是祖父的左孩子
// =============================================================
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right; // 叔叔节点是祖父的右孩子
// ---------------------------------------------------------
// Case 1:叔叔存在且为红色
// 策略:变色即可,无需旋转
// 原理:
// 1. 父变黑、叔变黑:解决 cur 和 parent 的连续红问题
// 2. 祖父变红:为了保持黑高不变(父+叔都变黑了,黑高会+2,祖父变红抵消+1)
// 3. 向上迭代:祖父变红后,可能和它的父节点形成新的连续红,所以把 cur 移到祖父继续检查
// ---------------------------------------------------------
if (uncle && uncle->_col == RED) {
parent->_col = BLACK; // 父变黑
uncle->_col = BLACK; // 叔变黑
grandfather->_col = RED; // 祖父变红(牺牲自己,维持黑高)
// 向上回溯,把祖父当作新的 cur,继续检查上层
cur = grandfather;
parent = cur->_parent;
}
// ---------------------------------------------------------
// Case 2 & 3:叔叔不存在 (NIL) 或叔叔为黑色
// 策略:必须通过旋转调整结构
// 原因:叔叔是黑的,如果只变色,会导致祖父这边的黑高变化,性质5崩溃
// ---------------------------------------------------------
else {
// -----------------------------------------------------
// Case 3:LR 型(折线形:cur 是 parent 的右孩子)
// 策略:先对 parent 左旋,把折线"拉直"成 LL 型
// 然后 swap(parent, cur):交换指针,让后续可以统一用 Case 2 的 LL 逻辑处理
// -----------------------------------------------------
if (cur == parent->_right) {
RotateL(parent); // 左旋 parent,cur 变成 parent 的父节点
std::swap(parent, cur); // 交换后,parent 指向原来的 cur,cur 指向原来的 parent
// 目的:统一后续代码逻辑,不需要再写一套 LR 的旋转变色
}
// -----------------------------------------------------
// Case 2:LL 型(直线形:cur 是 parent 的左孩子)
// 策略:右旋祖父 + 变色
// 原理:
// 1. 右旋祖父:让 parent 成为子树的新根,把连续红节点"拆开"
// 2. parent 变黑:作为新根,修复连续红问题
// 3. 祖父变红:作为 parent 的右孩子,维持黑高不变
// 4. break:旋转后 parent 变黑了,子树根是黑的,不会影响上层祖先,直接结束
// -----------------------------------------------------
RotateR(grandfather); // 右旋祖父
parent->_col = BLACK; // 新根变黑
grandfather->_col = RED; // 旧根变红
break; // 修复完成,无需向上回溯
}
}
// =============================================================
// 【右半区】父节点是祖父的右孩子(与左半区完全镜像对称)
// =============================================================
else {
Node* uncle = grandfather->_left; // 叔叔节点是祖父的左孩子
// ---------------------------------------------------------
// Case 1:叔叔存在且为红(镜像逻辑)
// ---------------------------------------------------------
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
// ---------------------------------------------------------
// Case 2 & 3:叔叔不存在或为黑(镜像逻辑)
// ---------------------------------------------------------
else {
// -----------------------------------------------------
// Case 3:RL 型(折线形:cur 是 parent 的左孩子)
// 策略:先右旋 parent,拉直成 RR 型,再 swap
// -----------------------------------------------------
if (cur == parent->_left) {
RotateR(parent);
std::swap(parent, cur);
}
// -----------------------------------------------------
// Case 2:RR 型(直线形:cur 是 parent 的右孩子)
// 策略:左旋祖父 + 变色
// -----------------------------------------------------
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
}
}
// --- 5. 最终保障:根节点强制染黑 ---
// 原因:如果在 Case 1 中把根节点染红了(cur 迭代到了根),这里必须强制改回黑
_root->_col = BLACK;
return true;
}
(注:红黑树中的 RotateL / RotateR 与 AVL 树的指针重连骨架 相同,都会维护左、右、父指针以及与上层的连接;但 AVL 旋转函数还需要维护 _bf,红黑树则维护颜色,因此两者不能把包含 BF 更新的完整函数体机械照搬。另:上面模拟代码中的 Case 1 注释"父+叔都变黑,黑高会 +2"应理解为左右两条对应路径各自增加 1 个黑节点,并不是同一条路径同时增加 2)
2.6 如何验证你的红黑树是对的?
写完红黑树,不要只看能不能跑通,必须写验证函数检查红黑性质。下面的 Check 主要验证:根为黑、无连续红节点、各路径黑高一致;"节点颜色只能红/黑"由枚举类型天然保证,而二叉搜索树的有序性依赖插入逻辑。若要做更完整的独立验证,还应额外检查中序序列是否有序。
cpp
bool IsValidRBTree() {
if (_root == nullptr) return true;
// 检查性质2:根是黑
if (_root->_col == RED) return false;
// 获取任意一条路径的黑节点数作为基准
int benchmarkBlackCount = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) benchmarkBlackCount++;
cur = cur->_left;
}
return _Check(_root, 0, benchmarkBlackCount);
}
bool _Check(Node* root, int blackCount, int benchmark) {
if (root == nullptr) {
// 检查性质5:路径黑节点数是否一致
return blackCount == benchmark;
}
if (root->_col == BLACK) blackCount++;
// 检查性质4:是否有连续红节点
if (root->_col == RED && root->_parent && root->_parent->_col == RED) {
cout << "错误:出现连续红节点!" << endl;
return false;
}
return _Check(root->_left, blackCount, benchmark) &&
_Check(root->_right, blackCount, benchmark);
}
补充:红黑树的查找
红黑树同样保留二叉搜索树的有序性质,所以 Find 本身仍按 BST 规则向左或向右搜索;红黑规则的作用是把树高限制在 O ( log N ) O(\log N) O(logN),从而使查找最坏复杂度保持为 O ( log N ) O(\log N) O(logN)。也就是说,红黑树复杂的是插入/删除后的平衡维护,而不是查找路径本身。
2.7 完整代码展示
以下是完整的、可编译运行的代码:
cpp
#pragma once
#include <iostream>
#include <vector>
#include <ctime> // 随机数测试
using namespace std;
// -------------------- 颜色枚举 --------------------
enum Colour { RED, BLACK };
// -------------------- 红黑树节点 --------------------
template<class K, class V>
struct RBTreeNode {
pair<K, V> _kv;
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Colour _col;
RBTreeNode(const pair<K, V>& kv)
: _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) {
/* 新节点默认红色:
插入黑节点会破坏"黑高一致"(性质5),难以全局修复;
插入红节点仅可能破坏"不红红"(性质4),局部修正即可。 */
}
};
// -------------------- 红黑树类 --------------------
template<class K, class V>
class RBTree {
typedef RBTreeNode<K, V> Node;
public:
RBTree() = default;
~RBTree() { Destroy(_root); }
// ---------- 插入核心 ----------
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; // key 已存在
}
cur = new Node(kv); // 红色新节点
if (parent->_kv.first < kv.first) parent->_right = cur;
else parent->_left = cur;
cur->_parent = parent;
// 3. 红黑树修复(父节点为红才需调整)
while (parent && parent->_col == RED) {
Node* grandpa = parent->_parent; // 祖父一定存在(父为红非根)
// ------ 父为祖父左孩子 ------
if (parent == grandpa->_left) {
Node* uncle = grandpa->_right;
// 情况1:叔叔为红 ------ 变色 + 向上继续
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
// 情况2 & 3:叔叔为黑(或不存在)
else {
// 情况3:cur 是 parent 的右孩子(折线) → 左旋变直线
if (cur == parent->_right) {
RotateL(parent);
swap(parent, cur); // 交换后 parent 成为新子树的根(黑色)
}
// 情况2:直线(LL) → 右旋 + 变色
RotateR(grandpa);
parent->_col = BLACK; // 新根变黑
grandpa->_col = RED; // 原祖父变红
break; // 调整结束
}
}
// ------ 父为祖父右孩子(对称) ------
else {
Node* uncle = grandpa->_left;
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
else {
if (cur == parent->_left) { // RL 折线
RotateR(parent);
swap(parent, cur);
}
RotateL(grandpa); // RR 直线
parent->_col = BLACK;
grandpa->_col = RED;
break;
}
}
}
_root->_col = BLACK; // 确保根黑
return true;
}
// ---------- 旋转(同AVL,仅改变指针,不处理颜色) ----------
void 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 (ppNode->_left == parent) ppNode->_left = subR;
else ppNode->_right = subR;
subR->_parent = ppNode;
}
}
void 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 (ppNode->_left == parent) ppNode->_left = subL;
else ppNode->_right = subL;
subL->_parent = ppNode;
}
}
// ---------- 验证红黑树 ----------
bool IsValid() {
if (_root == nullptr) return true;
if (_root->_col == RED) { cout << "根为红\n"; return false; }
// 取最左路径的黑节点数作为基准
int blackCnt = 0;
Node* cur = _root;
while (cur) {
if (cur->_col == BLACK) ++blackCnt;
cur = cur->_left;
}
return Check(_root, 0, blackCnt);
}
void InOrder() { InOrder(_root); cout << endl; }
int Height() { return Height(_root); }
int Size() { return Size(_root); }
private:
// ---------- 递归辅助 ----------
bool Check(Node* root, int curBlack, int bench) {
if (root == nullptr) {
if (curBlack != bench) { cout << "黑高不一致\n"; return false; }
return true;
}
if (root->_col == RED && root->_parent && root->_parent->_col == RED) {
cout << "连续红节点\n"; return false;
}
if (root->_col == BLACK) ++curBlack;
return Check(root->_left, curBlack, bench) &&
Check(root->_right, curBlack, bench);
}
void InOrder(Node* root) {
if (!root) return;
InOrder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << " ";
InOrder(root->_right);
}
int Height(Node* root) {
return root ? 1 + max(Height(root->_left), Height(root->_right)) : 0;
}
int Size(Node* root) {
return root ? 1 + Size(root->_left) + Size(root->_right) : 0;
}
void Destroy(Node* root) {
if (!root) return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
Node* _root = nullptr;
};
// -------------------- 测试 --------------------
int main() {
// 基础测试
int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};
RBTree<int, int> t1;
for (auto e : a) t1.Insert({e, e});
cout << "中序: "; t1.InOrder();
cout << "合法: " << (t1.IsValid() ? "是" : "否") << "\n\n";
// 压力测试
cout << "随机插入10000个数字...\n";
const int N = 10000;
vector<int> v(N);
srand((unsigned)time(0));
for (int i = 0; i < N; ++i) v[i] = rand();
RBTree<int, int> t2;
for (auto e : v) t2.Insert({e, e});
cout << "树高: " << t2.Height() << "\n节点数: " << t2.Size() << "\n";
cout << "合法: " << (t2.IsValid() ? "是" : "否") << "\n";
return 0;
}
2.8 总结
红黑树之所以难,是因为它将颜色约束 与旋转逻辑 耦合在了一起。但只要你抓住 "叔叔的颜色决定了是变色还是旋转" 这一核心,就能在脑海中构建出清晰的图谱。
第三章:红黑树的封装------泛型编程的艺术
这是一个非常关键的章节。如果说上一章的"红黑树实现"是造出了强劲的发动机 ,那么这一章就是通过巧妙的封装,造出跑车(Map)和越野车(Set)。
这一章的核心思想是:泛型编程(Generic Programming)与"通过策略/仿函数完成适配"的设计思想 。这里的 KeyOfT 更准确地说是一个"取 Key 的策略仿函数",不必把它严格等同于 GoF 的 Adapter 设计模式。我们需要改造红黑树,让它能够同时适配 key 场景(Set)和 key-value 场景(Map)。
set 和 map 都复用同一个 rb_tree 模板;GNU libstdc++ 的常见实现中对应结构名为 _Rb_tree。需要注意:这是具体标准库实现方式,C++ 标准并不规定 std::map/std::set 的底层必须是红黑树。
这怎么可能?
set存的是K(键值即数据)。map的节点值类型是pair<const K, V>(键不可通过迭代器修改,映射值V可以修改)。- 红黑树插入时需要比较大小:
set直接比K,但map需要比pair.first。
如果我们写两份红黑树代码,那就是代码冗余的灾难。今天,我们要学习如何通过模板参数控制 和仿函数(Functor),用同一棵树支撑起两个容器。
结合 SGI STL 源码理解三个核心模板角色:
Key:用于find/erase等按关键码操作的参数类型;Value:红黑树节点中真正存储的数据类型 。set的Value是Key,map的Value是pair<const Key, T>;KeyOfValue:从Value中提取比较用Key的策略。set相当于使用identity直接返回自身,map相当于使用select1st取pair.first。因此,即使
set中Key与Value看起来相同,这两个模板参数的职责 仍然不同;而在map中这种区别尤其明显:插入的是pair,查找/删除通常按Key进行。
3.1 改造节点结构:从 <K, V> 到 <T>
在上一章,我们的节点是写死的 pair<K, V>。现在为了通用,我们需要把节点里存的数据定义为 T。
- 对于 Set :
T就是K。 - 对于 Map :
T就是pair<const K, V>。
cpp
// 原来的写法(只适合 Map)
// template<class K, class V>
// struct RBTreeNode { pair<K, V> _kv; ... };
// 改造后的写法(通用)
template<class T>
struct RBTreeNode
{
T _data; // 如果是 set,这里存 key;如果是 map,这里存 pair
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Colour _col;
RBTreeNode(const T& data)
: _data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED)
{}
};
3.2 核心难点:如何比较大小?(KeyOfT)
红黑树的 Insert 和 Find 需要比较节点数据的大小。
Set:KeyOfT直接返回T自身(即 key),然后比较 key。Map:KeyOfT从T(即 pair)中提取first,始终只比较 key,而不是让pair的second参与排序。
底层的 RBTree 不知道上层是 Map 还是 Set,它只管存 T。谁知道怎么取 Key?上层使用者知道!
因此,我们需要在 RBTree 的模版参数中增加一个仿函数(Functor) ,我们称之为 KeyOfT。它的作用是从 T 中提取出用于比较的 Key。
RBTree 类的改造
cpp
// K: 关键码类型(用于查找)
// T: 节点中存储的数据类型
// KeyOfT: 仿函数,用于从 T 中提取 K
template<class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
bool Insert(const T& data)
{
KeyOfT kot; // 实例化仿函数
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
// --- 核心变化点 ---
// 以前:if (cur->_kv.first < kv.first)
// 现在:利用 kot 对象从 _data 中提取 key 进行比较
if (kot(cur->_data) < kot(data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(data))
{
parent = cur;
cur = cur->_left;
}
else return false;
}
// ... 后续插入与旋转逻辑不变 ...
}
// ...
};
3.3 迭代器 (Iterator) 的实现
STL 的精髓在于迭代器。map 和 set 的迭代器本质上就是红黑树的迭代器。红黑树的迭代器走的是中序遍历(有序)。
迭代器结构
我们需要实现 operator++ 和 operator--。
-
++逻辑:- 右子树存在 :下一个节点是右子树的最左节点(该右子树中的最小节点)。
- 右子树不存在 :沿父指针向上找,只要当前节点是父节点的右孩子,就说明父节点已经访问过;直到找到"当前节点是其父节点左孩子"的祖先,该父节点就是下一个节点。若一直找到根之上,则到达
end()。
-
--逻辑与之镜像:- 左子树存在 :上一个节点是左子树的最右节点(最大节点)。
- 左子树不存在:向上找,只要当前节点是父节点的左孩子就继续上移,直到找到"当前节点是其父节点右孩子"的祖先。
--end()是特殊情况 :标准双向迭代器要求从end()后退能到达整棵树的最右节点。模拟实现让迭代器额外保存_root来处理这一点;SGI STL 则使用header哨兵节点,header还会记录根、最左和最右节点。
cpp
template<class T, class Ref, class Ptr>
struct __RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef __RBTreeIterator<T, Ref, Ptr> Self;
Node* _node; // 迭代器当前指向的节点指针
__RBTreeIterator(Node* node) : _node(node) {}
// --- 常用操作重载 ---
// 解引用操作:返回节点数据的引用 (Ref)
// 如果是 iterator,Ref 是 T&;如果是 const_iterator,Ref 是 const T&
Ref operator*()
{
return _node->_data;
}
// 箭头操作:返回节点数据的指针 (Ptr)
// 使得迭代器可以像指针一样使用 -> 访问成员
// node:这是迭代器内部持有的红黑树节点指针。
//_node->_data:这是取出了节点里存的真实数据(例如 pair<const K, V>)。
//&:这是取地址。我们取出了 _data 这个数据的内存地址。也就是返回了一个指向数据的原生指针(T*)。
Ptr operator->()
{
return &_node->_data;
}
// 判断两个迭代器是否不等
bool operator!=(const Self& s)
{
return _node != s._node;
}
// 判断两个迭代器是否相等
bool operator==(const Self& s)
{
return _node == s._node;
}
// --- 核心逻辑:前置++ ---
// 作用:让迭代器指向中序遍历的下一个节点
// 中序遍历规则:左子树 -> 根节点 -> 右子树
// 当你对某个节点 curr 调用 ++ 时,说明 curr(根)已经被访问过了。接下来该去找谁?
Self& operator++()
{
if (_node->_right)
{
// 【情况 1】:右子树存在
// 根据中序遍历规则,访问完当前节点(根)后,接下来要访问右子树。
// 右子树中的第一个节点,一定是右子树中"最左边"的那个节点(即最小值)。
Node* leftMost = _node->_right; // 先走到右孩子
while (leftMost->_left) // 然后一直往左走到底
{
leftMost = leftMost->_left;
}
_node = leftMost; // 更新迭代器指向
}
else
{
// 【情况 2】:右子树不存在
// 说明当前节点(以及它的左子树)都已经访问过了。
// 我们需要向上回溯,寻找一个"还未被访问"的祖先节点。
Node* cur = _node;
Node* parent = cur->_parent;
// 循环判断:
// 如果 cur 是 parent 的【右孩子】:
// 说明 parent 已经在 cur 之前被访问过了(因为是中序:左->根->右),
// 所以 parent 不是我们要找的下一个节点,必须继续向上找。
while (parent && cur == parent->_right)
{
cur = parent;
parent = parent->_parent;
}
// 循环结束有两种可能:
// 1. parent 为空:说明 最开始的cur 是整棵树最右下的节点(根也是其左子树的一部分),遍历结束,_node 置为 nullptr。
// 2. cur == parent->_left:说明 cur 是 parent 的【左孩子】。
// 根据中序遍历(左->根->右),访问完左子树(cur所在的子树)后,下一个就是根节点(parent)。
_node = parent;
}
return *this;
}
// --- 前置-- (逻辑与 ++ 完全对称) ---
// 作用:让迭代器指向中序遍历的上一个节点
Self& operator--()
{
if (_node->_left)
{
// 【情况 1】:左子树存在
// 上一个节点是左子树中"最右边"的节点(最大值)
Node* rightMost = _node->_left;
while (rightMost->_right)
{
rightMost = rightMost->_right;
}
_node = rightMost;
}
else
{
// 【情况 2】:左子树不存在
// 向上回溯,只要 cur 是 parent 的左孩子,说明 parent 已经在后面了(因为是左->根),继续往上。
// 直到找到一个节点是其 parent 的"右孩子",该 parent 就是上一个节点。
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
};
将迭代器集成到 RBTree 中:
cpp
template<class K, class T, class KeyOfT>
class RBTree
{
// ...
typedef __RBTreeIterator<T, T&, T*> iterator;
typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
Node* left = _root;
while (left && left->_left) left = left->_left; // 找最左节点
return iterator(left);
}
iterator end() { return iterator(nullptr); }
// ...
};
注意: 上面这段"迭代器结构"是简化演示版。它把
end()表示为nullptr,但operator--()一开始会直接访问_node->_left,因此这段简化代码不能直接执行--end()。PDF 的完整版通过在迭代器中同时保存_root,并在_node == nullptr时跳到整棵树最右节点来补齐该语义;SGI STL 则借助header哨兵实现。
3.4 RBTree.h (核心底层)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;
// ============================================================================
// 1. 颜色枚举
// ============================================================================
enum Colour { RED, BLACK };
// ============================================================================
// 2. 红黑树节点结构
// ============================================================================
template<class T>
struct RBTreeNode
{
T _data; // 存储的数据(对于 map 是 pair,对于 set 是 key)
RBTreeNode<T>* _left; // 左孩子指针
RBTreeNode<T>* _right; // 右孩子指针
RBTreeNode<T>* _parent; // 父节点指针(红黑树调整时必须)
Colour _col; // 节点颜色
// 构造函数:用给定数据构造节点
// 重要:新节点默认颜色为 RED。
// 理由:插入黑色节点会直接破坏"所有路径黑节点数相同"(性质5),修复成本极高;
// 插入红色节点仅可能破坏"不能连续红节点"(性质4),可通过局部变色/旋转修复。
RBTreeNode(const T& data)
: _data(data)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{}
};
// ============================================================================
// 3. 红黑树迭代器(支持中序遍历顺序的 ++ 和 --)
// ============================================================================
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> Self;
Node* _node; // 迭代器当前所指向的节点指针
// 构造函数:用一个节点指针构造迭代器
// 这是迭代器唯一的构造方式,通常由红黑树的 Begin() / End() 或 Find() 返回。
RBTreeIterator(Node* node) : _node(node) {}
// 解引用运算符:返回节点数据的引用(可读可写,取决于 Ref 是 T& 还是 const T&)
Ref operator*() { return _node->_data; }
// 箭头运算符:返回节点数据的指针,便于用 it->first 访问 pair 的成员
Ptr operator->() { return &_node->_data; }
// 比较运算符:比较两个迭代器是否指向同一个节点
bool operator!=(const Self& s) const { return _node != s._node; }
bool operator==(const Self& s) const { return _node == s._node; }
// ---------- 前置++(中序遍历的后继) ----------
// 返回中序遍历中当前节点的下一个节点
Self& operator++()
{
if (_node->_right)
{
// 若右子树存在,则下一个节点是右子树中的最左节点(最小节点)
Node* leftMost = _node->_right;
while (leftMost->_left)
leftMost = leftMost->_left;
_node = leftMost;
}
else
{
// 右子树为空,向上回溯:
// 只要当前节点是父节点的右孩子,说明父节点已经访问过,继续上溯;
// 直到当前节点是父节点的左孩子,则父节点就是下一个节点。
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = parent;
parent = cur->_parent;
}
_node = parent; // 可能为 nullptr(到达 end)
}
return *this;
}
// ---------- 前置--(中序遍历的前驱) ----------
// 返回中序遍历中当前节点的上一个节点
Self& operator--()
{
if (_node->_left)
{
// 若左子树存在,则上一个节点是左子树中的最右节点(最大节点)
Node* rightMost = _node->_left;
while (rightMost->_right)
rightMost = rightMost->_right;
_node = rightMost;
}
else
{
// 左子树为空,向上回溯:
// 只要当前节点是父节点的左孩子,说明父节点在当前节点之后,继续上溯;
// 直到当前节点是父节点的右孩子,则父节点就是上一个节点。
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = cur->_parent;
}
_node = parent; // 可能为 nullptr(到达 begin 之前)
}
return *this;
}
};
// ============================================================================
// 4. 红黑树主体类
// ============================================================================
template<class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
// ---------- 迭代器类型别名 ----------
typedef RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeIterator<T, const T&, const T*> ConstIterator;
// ========================================================================
// 构造 / 析构 / 拷贝 / 赋值 (这些函数是对象生命周期管理的关键)
// ========================================================================
// 1. 默认构造函数
// 功能:构造一棵空树。
// 过程:编译器自动生成,但这里显式写出 = default,表示使用编译器合成的版本。
// 合成版本会调用成员变量 _root 的默认构造函数(对于指针,即初始化为 nullptr)。
// 注:由于 _root 是内置类型,默认初始化为随机值,但类内初始化(= nullptr)保证其为空。
RBTree() = default;
// 2. 拷贝构造函数
// 功能:用另一棵红黑树 t 构造当前树(深拷贝)。
// 过程:
// (1) 调用私有成员函数 Copy(t._root),递归拷贝所有节点。
// (2) Copy 函数会复制每个节点的数据、颜色,并重建父子关系。
// (3) 将新树的根赋给 _root。
// 注意:必须深拷贝,因为两个对象不能共享节点(否则析构会 double free)。
RBTree(const RBTree& t)
{
_root = Copy(t._root);
}
// 3. 赋值运算符重载(现代写法:Copy and Swap)
// 功能:将当前对象赋值为另一棵树 t 的副本。
// 过程:
// (1) 参数 t 是传值,这本身会调用拷贝构造(或移动构造)生成一个临时副本。
// (2) 交换当前对象的 _root 与临时对象 t 的 _root。
// (3) 函数返回后,临时对象 t 被销毁,其析构函数会释放原来当前对象的旧资源。
// (4) 当前对象现在拥有新树的节点。
// 优点:异常安全,代码简洁,自动处理自我赋值。
RBTree& operator=(RBTree t)
{
swap(_root, t._root); // 交换指针,t 现在持有旧资源
return *this;
}
// 4. 析构函数
// 功能:释放树中所有节点占用的内存。
// 过程:
// (1) 调用私有辅助函数 Destroy(_root),进行后序遍历删除每个节点。
// (2) 然后将 _root 置为 nullptr,防止野指针。
// 注意:Destroy 是递归的,对于深度很大的树可能递归过深,但红黑树高度为 O(logN),安全。
~RBTree()
{
Destroy(_root);
_root = nullptr;
}
// ========================================================================
// 迭代器接口
// ========================================================================
// 返回指向树中第一个节点(按中序遍历)的迭代器,即最左节点
Iterator Begin()
{
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return Iterator(leftMost);
}
// 返回结束迭代器(nullptr)
Iterator End() { return Iterator(nullptr); }
// const 版本的 Begin / End(用于 const 对象)
ConstIterator Begin() const
{
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return ConstIterator(leftMost);
}
ConstIterator End() const { return ConstIterator(nullptr); }
// ========================================================================
// 核心插入操作(返回插入位置迭代器 + 是否成功)
// ========================================================================
pair<Iterator, bool> Insert(const T& data)
{
// ---------- 空树情况 ----------
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK; // 根节点必须为黑色(性质2)
return make_pair(Iterator(_root), true);
}
KeyOfT kot; // 仿函数对象,用于从 data 中提取键值 key
Node* parent = nullptr;
Node* cur = _root;
// ---------- 1. 标准 BST 插入,寻找位置 ----------
while (cur)
{
if (kot(cur->_data) < kot(data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(data))
{
parent = cur;
cur = cur->_left;
}
else
{
// key 已存在,插入失败,返回已有节点迭代器和 false
return make_pair(Iterator(cur), false);
}
}
// ---------- 2. 创建新节点并链接到树中 ----------
cur = new Node(data); // 默认红色
Node* newnode = cur; // 备份新节点地址,用于返回值
if (kot(parent->_data) < kot(data))
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// ---------- 3. 红黑树平衡调整(修复红红冲突) ----------
// 当父节点存在且为红色时,需要修复
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
// ------- 情况 A:父节点是祖父的左孩子 -------
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
// Case 1:叔叔存在且为红色 → 变色,向上继续
if (uncle && uncle->_col == RED)
{
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent; // 继续向上检查
}
else
{
// Case 2 和 3:叔叔不存在或为黑色 → 旋转
// Case 3:当前节点是父节点的右孩子(LR 折线)→ 先左旋变 LL 直线
if (cur == parent->_right)
{
RotateL(parent);
swap(parent, cur); // 旋转后 parent 和 cur 角色互换,统一为 LL 处理
}
// Case 2:当前节点是父节点的左孩子(LL 直线)→ 右单旋 + 变色
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break; // 调整完毕,子树根变黑,不会影响上层
}
}
// ------- 情况 B:父节点是祖父的右孩子(对称) -------
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
{
// Case 3:当前节点是父节点的左孩子(RL 折线)→ 先右旋变 RR 直线
if (cur == parent->_left)
{
RotateR(parent);
swap(parent, cur);
}
// Case 2:当前节点是父节点的右孩子(RR 直线)→ 左单旋 + 变色
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
}
}
// ---------- 4. 确保根节点为黑色(性质2) ----------
_root->_col = BLACK;
// 返回新插入节点的迭代器和成功标志
return make_pair(Iterator(newnode), true);
}
// ========================================================================
// 查找操作
// ========================================================================
Iterator Find(const K& key)
{
KeyOfT kot;
Node* cur = _root;
while (cur)
{
if (kot(cur->_data) < key)
cur = cur->_right;
else if (kot(cur->_data) > key)
cur = cur->_left;
else
return Iterator(cur);
}
return End();
}
// ========================================================================
// 调试 & 验证接口
// ========================================================================
int Size() { return _Size(_root); }
int Height() { return _Height(_root); }
// 验证当前树是否满足红黑树的所有性质
bool IsBalance()
{
if (_root == nullptr) return true;
// 性质2:根节点必须为黑色
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);
}
private:
// ========================================================================
// 旋转操作(与 AVL 相同,但红黑树旋转不修改平衡因子,仅改变指针)
// ========================================================================
// 左单旋:用于 RR 型失衡
// 以 parent 为轴,将其右孩子 subR 提升为子树根,parent 变为 subR 的左孩子
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
// 1. subRL 过继给 parent 作为右孩子
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
// 2. parent 变成 subR 的左孩子
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
// 3. subR 接替原 parent 的位置
if (parentParent == nullptr)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
}
// 右单旋:用于 LL 型失衡(对称于左旋)
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;
}
}
// ========================================================================
// 递归检查红黑树性质(供 IsBalance 调用)
// ========================================================================
bool Check(Node* root, int blackNum, const int refNum)
{
if (root == nullptr)
{
// 到达叶子节点,检查当前路径的黑高是否与基准一致
if (refNum != blackNum)
{
cout << "错误:路径黑色节点数量不一致" << endl;
return false;
}
return true;
}
// 检查性质4:不能有连续红色节点
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
cout << "错误:存在连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK) ++blackNum;
// 递归检查左右子树
return Check(root->_left, blackNum, refNum) &&
Check(root->_right, blackNum, refNum);
}
// ========================================================================
// 内部辅助函数(递归实现)
// ========================================================================
// 计算节点个数
int _Size(Node* root)
{
return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}
// 计算树的高度(以根为第1层)
int _Height(Node* root)
{
if (root == nullptr) return 0;
int leftH = _Height(root->_left);
int rightH = _Height(root->_right);
return leftH > rightH ? leftH + 1 : rightH + 1;
}
// 后序遍历删除所有节点(析构时调用)
void Destroy(Node* root)
{
if (!root) return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
// 深拷贝辅助函数(前序遍历递归复制)
// 注意:必须正确维护新节点的父指针,否则树结构损坏
Node* Copy(Node* root)
{
if (root == nullptr) return nullptr;
// 拷贝当前节点(数据 + 颜色)
Node* newRoot = new Node(root->_data);
newRoot->_col = root->_col;
// 递归拷贝左子树
newRoot->_left = Copy(root->_left);
if (newRoot->_left) newRoot->_left->_parent = newRoot;
// 递归拷贝右子树
newRoot->_right = Copy(root->_right);
if (newRoot->_right) newRoot->_right->_parent = newRoot;
return newRoot;
}
// ========================================================================
// 成员变量
// ========================================================================
Node* _root = nullptr; // 根节点指针,类内初始化确保空树时指向 nullptr
};
3.5 封装 MySet
现在底层准备好了,MySet 的封装就变得非常简单,简直是"填空题"。PDF 的做法是让底层树直接实例化为 RBTree<K, const K, SetKeyOfT>,从存储类型层面保证 set 的 key 不能被修改;下面保留的模拟代码采取"底层存 K、对外只暴露 ConstIterator"的思路,目标同样是禁止通过 set 迭代器修改 key。
cpp
#pragma once
#include "RBTree.h"
namespace m
{
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key) { return key; }
};
public:
typedef RBTree<K, K, SetKeyOfT> Tree;
// Set 的迭代器不可被修改,所以 iterator 和 const_iterator 都是 const 的
typedef typename Tree::ConstIterator iterator;
typedef typename Tree::ConstIterator const_iterator;
pair<iterator, bool> insert(const K& k)
{
// 底层 insert 返回的是 pair<普通iterator, bool>
// 这里可能涉及普通迭代器转 const 迭代器,实际编译器通常支持隐式转换
// 或者需要手动构造 pair
auto ret = _t.Insert(k);
return pair<iterator, bool>(ret.first, ret.second);
}
iterator begin() const { return _t.Begin(); }
iterator end() const { return _t.End(); }
iterator find(const K& k) const { return _t.Find(k); }
private:
Tree _t;
};
}
3.6 封装 MyMap
MyMap 稍微复杂一点点,主要体现在 operator[] 的支持上。为了实现 operator[],底层 Insert 不能只返回成功/失败,而要返回 pair<iterator, bool>:iterator 指向"新插入节点或已存在节点",bool 表示是否真的发生了插入。这样 operator[] 可以统一执行 insert(make_pair(key, V())),再返回 ret.first->second:key 不存在时插入一个默认构造的 V(),key 已存在时直接取得原节点的 second。
cpp
#pragma once
#include "RBTree.h"
namespace m
{
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv) { return kv.first; }
};
public:
// Map 的 Key 是 const 的,Value 是可修改的
typedef RBTree<K, pair<const K, V>, MapKeyOfT> Tree;
typedef typename Tree::Iterator iterator;
typedef typename Tree::ConstIterator const_iterator;
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
// 方括号重载:不存在则插入默认值,存在则返回 Value 引用
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
const_iterator begin() const { return _t.Begin(); }
const_iterator end() const { return _t.End(); }
iterator find(const K& k) { return _t.Find(k); }
private:
Tree _t;
};
}
3.7 总结与思考
通过这一章的封装,你完成了从"数据结构"到"容器库"的飞跃。
一定要理解的逻辑闭环:
- 用户 调用
MyMap。 MyMap定义了MapKeyOfT(如何从 pair 取 key)。MyMap实例化底层的RBTree<K, pair<const K, V>, MapKeyOfT>。RBTree在插入时,不知道数据是 pair,但它调用kot(data),不管是int还是pair,它最终都拿到了key进行比较。
这就是 C++ STL 极致复用的智慧。