本文基于 SGI-STL 3.0 源码框架,从零开始用红黑树封装实现
map和set,涵盖泛型设计思想、迭代器中序遍历、插入平衡调整(变色+旋转)、operator[]原理、const_iterator与 key 不可修改等核心知识点。所有代码均可直接编译运行,适合 C++ 进阶与数据结构面试备考。
从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)
-
- [一、前言:为什么要亲手封装 map 和 set?](#一、前言:为什么要亲手封装 map 和 set?)
- [二、SGI-STL 源码框架揭秘](#二、SGI-STL 源码框架揭秘)
-
- [2.1 头文件依赖关系](#2.1 头文件依赖关系)
- [2.2 rb_tree 的五模板参数设计:各司其职](#2.2 rb_tree 的五模板参数设计:各司其职)
- [2.3 节点结构:基类与派生类](#2.3 节点结构:基类与派生类)
- 三、红黑树基础快速回顾
-
- [3.1 红黑树的五条性质](#3.1 红黑树的五条性质)
- [3.2 为什么新插入的节点默认是红色?](#3.2 为什么新插入的节点默认是红色?)
- 四、红黑树节点与迭代器设计
-
- [4.1 节点结构 RBTreeNode](#4.1 节点结构 RBTreeNode)
- [4.2 迭代器设计思想](#4.2 迭代器设计思想)
- [4.3 operator++ 深度解析:中序遍历的下一个节点](#4.3 operator++ 深度解析:中序遍历的下一个节点)
- [4.4 operator-- 深度解析:中序遍历的上一个节点](#4.4 operator-- 深度解析:中序遍历的上一个节点)
- [4.5 SGI 源码的哨兵位 header vs 我们的 nullptr](#4.5 SGI 源码的哨兵位 header vs 我们的 nullptr)
- 五、红黑树核心实现
-
- [5.1 插入 Insert:完整流程拆解](#5.1 插入 Insert:完整流程拆解)
-
- [第一步:BST 搜索插入位置](#第一步:BST 搜索插入位置)
- 第二步:创建红节点并链接
- [第三步:调整平衡(while 循环)](#第三步:调整平衡(while 循环))
- 插入调整四种旋转场景总结表
- [5.2 旋转操作:核心指针交接](#5.2 旋转操作:核心指针交接)
-
- [左旋转 RotateL](#左旋转 RotateL)
- [5.3 查找 Find](#5.3 查找 Find)
- [5.4 析构与内存管理](#5.4 析构与内存管理)
- [5.5 Begin / End](#5.5 Begin / End)
- [六、封装 my_set](#六、封装 my_set)
-
- [6.1 完整实现](#6.1 完整实现)
- [6.2 为什么第二个模板参数是 const K?](#6.2 为什么第二个模板参数是 const K?)
- [七、封装 my_map](#七、封装 my_map)
-
- [7.1 完整实现](#7.1 完整实现)
- [7.2 为什么 pair 的 first 是 const K?](#7.2 为什么 pair 的 first 是 const K?)
- [7.3 operator\[\] 的实现原理与副作用](#7.3 operator[] 的实现原理与副作用)
- [八、const_iterator 与 key 不可修改的完整机制](#八、const_iterator 与 key 不可修改的完整机制)
-
- [8.1 迭代器模板参数 Ref / Ptr 的设计](#8.1 迭代器模板参数 Ref / Ptr 的设计)
- [8.2 set vs map 的迭代器修改权限对比](#8.2 set vs map 的迭代器修改权限对比)
- [8.3 const 对象只能用 const_iterator](#8.3 const 对象只能用 const_iterator)
- 九、易错点与深度思考汇总
-
- [9.1 --end() 的特殊处理](#9.1 --end() 的特殊处理)
- [9.2 依赖模板参数的类型必须加 typename](#9.2 依赖模板参数的类型必须加 typename)
- [9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?](#9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?)
- [9.4 迭代器失效问题](#9.4 迭代器失效问题)
- 十、完整源码汇总
-
- [10.1 RBTree.h](#10.1 RBTree.h)
- [10.2 my_set.h](#10.2 my_set.h)
- [10.3 my_map.h](#10.3 my_map.h)
- [10.4 测试代码](#10.4 测试代码)
- 十一、总结
一、前言:为什么要亲手封装 map 和 set?
在 C++ STL 中,std::map 和 std::set 是每天都在用的容器,但大多数人只停留在"会用"的层面。面试时一旦被追问:
map和set底层是什么数据结构?为什么选它?- 迭代器
++/--是怎么实现中序遍历的? operator[]为什么有副作用?- 为什么
set的迭代器不能修改元素,而map的迭代器能修改value却不能修改key? - 红黑树插入后是如何通过变色和旋转维持平衡的?
这些问题的答案,都藏在 SGI-STL 的 stl_tree.h、stl_map.h、stl_set.h 三个头文件里。
本文的目标不是简单贴源码,而是分层递进地把整个设计讲透:
- 先看懂 SGI-STL 的泛型框架设计(为什么一颗红黑树能同时服务 map 和 set);
- 再深入红黑树的节点、迭代器、插入与旋转实现;
- 最后用我们自己的红黑树封装出
my_map和my_set,并补充易错点与深度思考。
📖 前置推荐阅读
本篇聚焦于如何将红黑树封装为
map/set容器,侧重于泛型设计与迭代器实现 。如果你对红黑树的基础概念(五条性质、插入调整的变色与旋转)还不够熟悉,建议先阅读我的另一篇博客 ------ 《红黑树(Red-Black Tree)从原理到C++实现|面试深度解析》,那篇从零实现了红黑树的插入与旋转,是理解本文封装逻辑的必要前置。
二、SGI-STL 源码框架揭秘
2.1 头文件依赖关系
SGI-STL 3.0 中,map 和 set 的实现分布在以下头文件:
| 容器 | 头文件 | 底层依赖 |
|---|---|---|
set / multiset |
stl_set.h / stl_multiset.h |
stl_tree.h(红黑树) |
map / multimap |
stl_map.h / stl_multimap.h |
stl_tree.h(红黑树) |
核心关系:map 和 set 都不自己实现树,而是持有一个 rb_tree 成员对象,所有操作转发给它。
cpp
// stl_set.h 节选
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:
typedef Key key_type;
typedef Key value_type; // set 的 value_type 就是 Key 本身
private:
typedef rb_tree<key_type, value_type,
identity<value_type>, key_compare, Alloc> rep_type;
rep_type t; // 红黑树成员对象
};
// stl_map.h 节选
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:
typedef Key key_type;
typedef T mapped_type;
typedef pair<const Key, T> value_type; // map 的 value_type 是 pair
private:
typedef rb_tree<key_type, value_type,
select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t; // 红黑树成员对象
};
2.2 rb_tree 的五模板参数设计:各司其职
rb_tree 的模板签名是理解整个框架的钥匙:
cpp
template <class Key, class Value, class KeyOfValue,
class Compare, class Alloc = alloc>
class rb_tree;
| 参数 | 含义 | set 实例化时传什么 | map 实例化时传什么 |
|---|---|---|---|
Key |
键的类型,用于 find / erase 的形参 |
Key |
Key |
Value |
节点中实际存储的数据类型 | Key |
pair<const Key, T> |
KeyOfValue |
从 Value 中取出 Key 的仿函数 | identity<Key> |
select1st<pair> |
Compare |
比较器 | less<Key> |
less<Key> |
Alloc |
空间配置器 | alloc |
alloc |
设计精髓 :红黑树根本不关心自己存的是单纯的 key,还是 key/value 对------这完全由第二个模板参数 Value 决定。节点里存什么,Value 就是什么。
set:节点存Key,所以Value = Key;map:节点存pair<const Key, T>,所以Value = pair<const Key, T>。
为什么需要 KeyOfValue 仿函数?
因为节点里存的数据类型 T 是泛型的(可能是 int,也可能是 pair<int, string>),树本身不知道怎么从 T 里拿出 key 来比较。解决方案:由上层传入一个仿函数,告诉树"怎么从数据里取 key"。
cpp
// map 传入的仿函数:从 pair 中取 first(即 key)
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) {
return kv.first;
}
};
// set 传入的仿函数:key 就是数据本身
struct SetKeyOfT {
const K& operator()(const K& key) {
return key;
}
};
易错点 :
std::pair的默认operator<会同时比较first和second(字典序)。如果直接用pair比较,当 key 相同但 value 不同时会错误地认为两个节点不相等。所以必须用 KeyOfT 只提取 key 来比较。
为什么还要第一个模板参数 Key?
Key 是给 find 和 erase 做形参类型用的:
cpp
// stl_tree.h 节选
pair<iterator, bool> insert_unique(const value_type& x); // insert 用 Value
size_type erase(const key_type& x); // erase 用 Key
iterator find(const key_type& x); // find 用 Key
insert时传的是完整数据(Value);find/erase时只需要传 key(Key)。
对 set 来说 Key == Value,看起来重复;但对 map 来说,insert 传 pair,find 只传 Key,两者完全不同。
2.3 节点结构:基类与派生类
SGI-STL 把节点拆成了基类和派生类两层:
cpp
// 基类:只存颜色和三个指针,不涉及数据类型
struct __rb_tree_node_base {
typedef __rb_tree_color_type color_type;
typedef __rb_tree_node_base* base_ptr;
color_type color;
base_ptr parent;
base_ptr left;
base_ptr right;
};
// 派生类:增加数据字段
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base {
typedef __rb_tree_node<Value>* link_type;
Value value_field; // 实际存储的数据
};
为什么要拆成两层? 因为旋转、变色等操作只需要操作指针和颜色,不需要知道数据类型。基类指针 base_ptr 可以在不涉及模板的代码中通用,减少代码膨胀。我们自己实现时为了简洁,可以合并成一个模板结构体。

三、红黑树基础快速回顾
在动手实现之前,先把红黑树的核心性质过一遍,这是后续插入调整的理论基础。
3.1 红黑树的五条性质
- 每个节点不是红色就是黑色;
- 根节点是黑色;
- 每个叶子节点(NIL 空节点)是黑色;
- 如果一个节点是红色,它的两个孩子都是黑色(不能有连续的红色节点);
- 从任一节点到其每个叶子的所有路径,包含相同数量的黑色节点(黑高相同)。
满足这五条性质的红黑树,能保证最长路径不超过最短路径的 2 倍,从而保证 O(log n) 的查找、插入、删除效率。
3.2 为什么新插入的节点默认是红色?
新节点插入时,如果默认黑色,会导致所有经过该节点的路径黑高 +1,必然破坏性质 5,调整起来非常麻烦。
如果默认红色:
- 若父亲是黑色 → 五条性质全部满足,无需任何调整;
- 若父亲是红色 → 只破坏性质 4(连续红节点),只需局部调整。
所以新节点默认红色是最优选择。
四、红黑树节点与迭代器设计
4.1 节点结构 RBTreeNode
我们的实现不搞基类派生,直接用一个模板结构体:
cpp
enum Colour {
RED,
BLACK
};
template<class T>
struct RBTreeNode {
T _data; // 节点存储的数据(set中是K,map中是pair<const K,V>)
RBTreeNode<T>* _left; // 左孩子
RBTreeNode<T>* _right; // 右孩子
RBTreeNode<T>* _parent; // 父指针------迭代器和旋转都需要它
Colour _col; // 颜色
RBTreeNode(const T& data)
: _data(data)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
为什么必须有
_parent指针? 两个原因:
- 迭代器
++/--需要从当前节点向上找祖先;- 旋转操作 需要知道父节点的父节点(grandfather),以及更新各个指针的指向。
如果没有 parent 指针,每次都要从根遍历找父亲,效率会从 O(1) 退化为 O(log n)。
4.2 迭代器设计思想
迭代器的本质是对节点指针的封装 ,通过重载运算符让它表现得像指针一样。和 list 的迭代器思路完全一致。
cpp
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> Self;
Node* _node; // 当前指向的节点
Node* _root; // 保存根指针,用于 --end() 的特殊处理
RBTreeIterator(Node* node, Node* root)
: _node(node), _root(root)
{}
// 解引用:返回数据的引用
Ref operator*() { return _node->_data; }
// 箭头:返回数据的指针
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; }
};
三个模板参数的含义:
| 参数 | 普通迭代器 iterator |
常量迭代器 const_iterator |
|---|---|---|
T |
数据类型 | 数据类型 |
Ref |
T& |
const T& |
Ptr |
T* |
const T* |
通过 Ref 和 Ptr 的不同实例化,一套迭代器代码同时支持普通和常量两种迭代器,这是 STL 的经典泛型技巧。
在红黑树类中定义:
cpp
typedef RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeIterator<T, const T&, const T*> ConstIterator;
4.3 operator++ 深度解析:中序遍历的下一个节点
map 和 set 的迭代器走的是中序遍历 (左 → 根 → 右),所以 ++it 要找到中序序列中的下一个节点。
只看局部,分两种情况:
情况一:右子树不为空
当前节点访问完了,下一个要访问的是右子树的中序第一个节点 ,也就是右子树的最左节点(最小值)。
30
/ \
15 40
/ \ \
10 25 50
it 指向 30,右子树不为空 → 找右子树最左节点 40
it 指向 15,右子树不为空 → 找右子树最左节点 25
cpp
if (_node->_right) {
Node* min = _node->_right;
while (min->_left) {
min = min->_left;
}
_node = min;
}
情况二:右子树为空
当前节点和它所在的子树都访问完了,下一个节点在祖先 中。沿着 parent 向上找,直到找到"孩子是父亲的左孩子"的那个祖先------根据中序遍历,左子树访问完后就该访问根(父亲)了。
18
/ \
10 30
\ / \
15 25 40
it 指向 25,右为空,25 是 30 的左孩子 → 下一个是 30
it 指向 15,右为空,15 是 10 的右孩子 → 继续向上,10 是 18 的左孩子 → 下一个是 18
it 指向 40,右为空,40 是 30 的右 → 30 是 18 的右 → 18 无父 → 下一个是 nullptr(end)
cpp
else {
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right) {
cur = parent;
parent = cur->_parent;
}
_node = parent; // 可能为 nullptr(即 end())
}
end() 的表示 :我们用
nullptr充当end()。当中序遍历到最后一个节点(最大节点)再++时,向上找祖先的循环最终会让parent变为nullptr,迭代器自然就变成了end()。

4.4 operator-- 深度解析:中序遍历的上一个节点
--it 是 ++it 的镜像,访问顺序反过来(右 → 根 → 左),但有一个特殊情况需要单独处理。
特殊情况:--end()
end() 指向 nullptr,对它执行 -- 应该走到中序最后一个节点,也就是整棵树的最右节点 (最大值)。这就是迭代器需要保存 _root 指针的原因。
cpp
if (_node == nullptr) { // --end()
Node* rightMost = _root;
while (rightMost && rightMost->_right) {
rightMost = rightMost->_right;
}
_node = rightMost;
}
情况一:左子树不为空
上一个节点是左子树的中序最后一个节点 ,也就是左子树的最右节点(最大值)。
cpp
else if (_node->_left) {
Node* rightMost = _node->_left;
while (rightMost->_right) {
rightMost = rightMost->_right;
}
_node = rightMost;
}
情况二:左子树为空
沿着 parent 向上找,直到找到"孩子是父亲的右孩子"的那个祖先。
cpp
else {
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left) {
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
4.5 SGI 源码的哨兵位 header vs 我们的 nullptr
SGI-STL 的实现比我们更精巧,它用了一个哨兵位头节点(header) 来充当 end(),而不是 nullptr:
| 对比项 | SGI-STL(header 哨兵位) | 我们的实现(nullptr) |
|---|---|---|
| end() 表示 | header 节点 | nullptr |
| header 与根的关系 | 互为 parent | 无 |
| header->left | 指向最左节点(begin) | 无 |
| header->right | 指向最右节点 | 无 |
| --end() 实现 | node = node->right(直接取 header 的右) |
需从 root 遍历找最右节点 |
| 代码复杂度 | 较高 | 较低 |
SGI 的 decrement() 里有一段很巧妙的判断:
cpp
void decrement() {
if (node->color == __rb_tree_red &&
node->parent->parent == node)
node = node->right; // --end():header 的右就是最右节点
else if (node->left != 0) {
// ... 左子树最右节点
} else {
// ... 向上找祖先
}
}
header 节点是红色的,且 header->parent->parent == header(根的父亲是 header,header 的父亲是根),所以用这个条件判断当前是否是 end()。我们用 nullptr 方案虽然 --end() 多了一次遍历,但逻辑更直观,功能完全等价。
五、红黑树核心实现
5.1 插入 Insert:完整流程拆解
插入是红黑树最核心的操作,整体思路是:先按 BST 规则找到插入位置,创建红色新节点并链接;然后从新节点开始,沿祖先向上检查,若父节点为红则通过变色或旋转调整,直到满足红黑树性质;最后强制根变黑。
第一步:BST 搜索插入位置
从根节点开始,按照二叉搜索树的规则向下搜索:
cpp
template<class K, class T, class KeyOfT>
class RBTree {
typedef RBTreeNode<T> Node;
public:
pair<Iterator, bool> Insert(const T& data) {
// 空树:直接创建根节点,根必须是黑色
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK;
return { Iterator(_root, _root), true };
}
KeyOfT kot;
Node* parent = nullptr;
Node* cur = _root;
// 按 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 已存在,插入失败,返回已有节点的迭代器
return { Iterator(cur, _root), false };
}
}
注意 :这里比较用的是
kot(cur->_data)而不是cur->_data,因为T可能是pair,必须通过仿函数提取 key 来比较。
第二步:创建红节点并链接
cpp
cur = new Node(data);
Node* newnode = cur; // 保存新节点指针,用于返回
cur->_col = RED; // 新节点默认红色
// 链接到父节点
if (kot(parent->_data) < kot(data)) {
parent->_right = cur;
} else {
parent->_left = cur;
}
cur->_parent = parent; // 别忘了链接父指针!
易错点 :创建新节点后,除了设置父节点的
left/right,必须同时设置新节点的_parent。漏掉这一步会导致后续迭代器和旋转全部出错。
第三步:调整平衡(while 循环)
如果父亲是黑色,性质 4 没有被破坏,无需调整。只有当父亲是红色时,才需要进入调整循环。
调整时需要三个角色:cur(当前节点)、parent(父亲)、grandfather(祖父)。根据父亲是祖父的左孩子还是右孩子,分为两大分支,每个分支下又根据叔叔的颜色分情况。
cpp
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
// ========== 分支A:父亲是祖父的左孩子 ==========
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
// 情况1:叔叔存在且为红 → 变色,继续向上
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather; // 继续向上处理
parent = cur->_parent;
}
// 情况2:叔叔不存在或为黑 → 旋转+变色
else {
if (cur == parent->_left) {
// 左左:右单旋
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
} else {
// 左右:先左旋再右旋
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 旋转后平衡已恢复,无需继续向上
}
}
// ========== 分支B:父亲是祖父的右孩子(镜像) ==========
else {
Node* uncle = grandfather->_left;
// 情况1:叔叔存在且为红 → 变色,继续向上
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
// 情况2:叔叔不存在或为黑 → 旋转+变色
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 { Iterator(newnode, _root), true };
}
插入调整四种旋转场景总结表
| 场景 | 父亲位置 | cur 位置 | 叔叔状态 | 操作 | 变色规则 |
|---|---|---|---|---|---|
| 左左 | 祖父左 | 父亲左 | 不存在/黑 | 祖父右旋 | 父变黑,祖父变红 |
| 左右 | 祖父左 | 父亲右 | 不存在/黑 | 父左旋+祖父右旋 | cur变黑,祖父变红 |
| 右右 | 祖父右 | 父亲右 | 不存在/黑 | 祖父左旋 | 父变黑,祖父变红 |
| 右左 | 祖父右 | 父亲左 | 不存在/黑 | 父右旋+祖父左旋 | cur变黑,祖父变红 |
| 变色 | 任意 | 任意 | 存在且红 | 仅变色不旋转 | 父和叔变黑,祖父变红,继续向上 |
记忆口诀:叔叔红就变色往上走;叔叔黑就旋转,单旋变父色,双旋变 cur 色。
为什么变色情况要继续向上,而旋转情况可以 break?
- 变色后祖父变成了红色,如果祖父的父亲也是红色,就会再次出现连续红节点,所以必须继续向上检查。
- 旋转后,子树的根(原来的 parent 或 cur)变成了黑色,这保证了不会再和上层产生连续红节点,所以可以直接 break。
5.2 旋转操作:核心指针交接
旋转是红黑树维持平衡的核心手段。我们以左旋转为例,看关键指针的变化(完整实现见文末附录)。
左旋转 RotateL
左旋转是把"右孩子提上来,当前节点降为左孩子"。
parent subR
/ \ / \
a subR ===> parent c
/ \ / \
b c a b
关键指针交接(仅 4 行核心代码):
cpp
parent->_right = subRL; // subR 的左子树过继给 parent
subR->_left = parent; // parent 降为 subR 的左孩子
// 更新父指针(略,详见附录)
五步拆解(理解指针如何变动):
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | 保存 subR 和 subRL |
防止指针覆盖后丢失 |
| 2 | parent->_right = subRL,更新 subRL->_parent |
右孩子的左子树过继给 parent |
| 3 | 保存 parentParent |
旋转后需要让 subR 接替 parent 的位置 |
| 4 | subR->_left = parent,parent->_parent = subR |
parent 降为 subR 的左孩子 |
| 5 | 根据 parentParent 更新根或父子关系 | subR 正式接替 parent 的位置 |
右旋转是左旋转的镜像,不再赘述。完整的 RotateL 和 RotateR 函数实现见文末 第十章 完整源码汇总。
旋转操作易错点:
subRL/subLR可能为nullptr,更新父指针前必须判空;parentParent可能为nullptr(parent 是根节点),此时要更新_root;- 父指针
_parent必须同步更新,只改left/right不改_parent会导致树结构断裂;- 旋转只改变结构,不改变中序遍历序列------这是旋转的基本性质。
5.3 查找 Find
查找就是标准的 BST 搜索,利用 KeyOfT 提取 key 比较:
cpp
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, _root);
}
return End();
}
5.4 析构与内存管理
红黑树是动态内存结构,析构时需要后序遍历释放所有节点:
cpp
~RBTree() {
Destroy(_root);
_root = nullptr;
}
void Destroy(Node* root) {
if (root == nullptr)
return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
必须用后序遍历(左 → 右 → 根),因为要先释放孩子再释放自己。如果先释放根,孩子指针就丢失了。
5.5 Begin / End
cpp
Iterator Begin() {
Node* leftMost = _root;
while (leftMost && leftMost->_left) {
leftMost = leftMost->_left;
}
return Iterator(leftMost, _root); // 最左节点 = 中序第一个
}
Iterator End() {
return Iterator(nullptr, _root); // nullptr 充当 end
}
Begin() 返回最左节点(中序最小值),End() 返回 nullptr。const 版本同理,返回 ConstIterator。
六、封装 my_set
有了泛型红黑树,封装 set 就非常简单了------本质是一层接口转发。
6.1 完整实现
cpp
#pragma once
#include "RBTree.h"
namespace bit {
template<class K>
class set {
// 仿函数:set 的数据就是 key 本身
struct SetKeyOfT {
const K& operator()(const K& key) {
return key;
}
};
public:
// 迭代器类型转发(注意 typename 关键字)
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
const_iterator begin() const { return _t.Begin(); }
const_iterator end() const { return _t.End(); }
pair<iterator, bool> insert(const K& key) {
return _t.Insert(key);
}
iterator find(const K& key) {
return _t.Find(key);
}
private:
// 第二个模板参数是 const K ------ 保证 set 的元素不可修改
RBTree<K, const K, SetKeyOfT> _t;
};
}
6.2 为什么第二个模板参数是 const K?
这是一个关键设计。set 中的元素就是 key,而 key 是排序的依据------如果允许修改 key,树的有序性就被破坏了。
通过把第二个模板参数设为 const K,红黑树节点中存储的就是 const K,迭代器解引用得到的是 const K&,自然就无法修改了:
cpp
bit::set<int> s;
s.insert(5);
auto it = s.begin();
// *it = 10; // 编译错误:不能给常量赋值
易错点 :
typedef中依赖模板参数的类型必须加typename关键字。RBTree<K, const K, SetKeyOfT>::Iterator是一个依赖名,编译器在实例化前不知道它是类型还是静态成员,所以必须用typename显式声明。
七、封装 my_map
7.1 完整实现
cpp
#pragma once
#include "RBTree.h"
namespace bit {
template<class K, class V>
class map {
// 仿函数:从 pair 中提取 key(first)
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) {
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
const_iterator begin() const { return _t.Begin(); }
const_iterator end() const { return _t.End(); }
pair<iterator, bool> insert(const pair<K, V>& kv) {
return _t.Insert(kv);
}
iterator find(const K& key) {
return _t.Find(key);
}
// operator[]:核心功能
V& operator[](const K& key) {
pair<iterator, bool> ret = insert({ key, V() });
return ret.first->second;
}
private:
// pair 的 first 是 const K ------ key 不可修改,value 可修改
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
7.2 为什么 pair 的 first 是 const K?
和 set 的道理一样:map 的 key 是排序依据,不能修改。但 map 的 value 是可以修改的。
通过 pair<const K, V>,迭代器解引用得到 pair<const K, V>&:
it->first是const K,不可修改;it->second是V,可以修改。
cpp
bit::map<string, string> dict;
dict.insert({ "sort", "排序" });
auto it = dict.begin();
// it->first = "xxx"; // 编译错误:first 是 const
it->second = "排序,分类"; // OK:second 可以修改
7.3 operator\[\] 的实现原理与副作用
operator[] 是 map 最常用也最容易误用的接口。它的实现只有两行:
cpp
V& operator[](const K& key) {
pair<iterator, bool> ret = insert({ key, V() });
return ret.first->second;
}
工作流程:
- 调用
insert({key, V()})尝试插入一个 key 对应默认值的 pair; insert返回pair<iterator, bool>:- 如果 key 不存在 → 插入成功,
bool = true,迭代器指向新插入的节点; - 如果 key 已存在 → 插入失败,
bool = false,迭代器指向已有节点;
- 如果 key 不存在 → 插入成功,
- 返回该节点 value 的引用
ret.first->second。
三种典型用法:
cpp
bit::map<string, int> countMap;
// 用法1:插入 + 修改
countMap["apple"] = 5; // key 不存在,插入默认值0,再赋值为5
// 用法2:读取(key 必须存在,否则会意外插入)
int n = countMap["apple"]; // key 存在,返回 5
// 用法3:计数统计(最经典)
countMap["banana"]++; // key 不存在,插入默认值0,再++变成1
重大易错点:operator\[\] 有副作用!
如果你只是想查询一个 key 是否存在,绝对不要用
if (m[key])或cout << m[key],因为即使 key 不存在,operator[]也会插入一个默认值。这会导致:
- 容器意外变大;
- 后续
size()结果不符合预期;- 多线程环境下引发数据竞争。
正确的查询方式是用
find():
cppauto it = countMap.find("apple"); if (it != countMap.end()) { // key 存在,it->second 是值 }
扩展:multiset / multimap 只差一行代码
STL 中不仅有
set/map,还有multiset/multimap------它们与set/map的唯一区别是允许重复的 key 。对应到红黑树层面,rb_tree提供了两条插入路径:
insert_unique():key 已存在则插入失败(set/map使用);insert_equal():key 已存在仍插入到右子树(multiset/multimap使用)。在我们的
RBTree::Insert实现中,只需将相等判断分支从return { Iterator(cur, _root), false };改为parent = cur; cur = cur->_right;让相同 key 的节点始终向右走,即可支持重复键。理解了这一点,multiset/multimap的封装只是换一个Insert接口调用,其余代码完全复用。
八、const_iterator 与 key 不可修改的完整机制
8.1 迭代器模板参数 Ref / Ptr 的设计
我们的迭代器用三个模板参数 T, Ref, Ptr 来同时支持普通和常量迭代器:
cpp
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
Ref operator*() { return _node->_data; } // Ref 决定返回普通引用还是 const 引用
Ptr operator->() { return &_node->_data; } // Ptr 决定返回普通指针还是 const 指针
};
在红黑树中定义两种迭代器:
cpp
typedef RBTreeIterator<T, T&, T*> Iterator; // 普通迭代器
typedef RBTreeIterator<T, const T&, const T*> ConstIterator; // 常量迭代器
当 T = const K(set 的情况)时,即使是普通 Iterator,Ref 也是 const K&,元素依然不可修改。这就是"set 的普通迭代器也不能修改元素"的底层原因。
8.2 set vs map 的迭代器修改权限对比
| 容器 | 节点存储类型 T | 普通迭代器解引用 | 能否修改 key | 能否修改 value |
|---|---|---|---|---|
set |
const K |
const K& |
不能(无 value) | 无 value |
map |
pair<const K, V> |
pair<const K, V>& |
不能(first 是 const) | 能(second 非 const) |
8.3 const 对象只能用 const_iterator
cpp
void Print(const bit::set<int>& s) {
// s 是 const 引用,begin()/end() 返回 const_iterator
bit::set<int>::const_iterator it = s.begin();
while (it != s.end()) {
cout << *it << " ";
++it;
}
cout << endl;
}
如果这里用 iterator 而不是 const_iterator,编译会报错------因为 const 对象的 begin() 返回的是 const_iterator,不能隐式转换为 iterator(反向转换可以:普通迭代器可以隐式转为常量迭代器)。
九、易错点与深度思考汇总
本节精选 正文尚未强调过的 5 个易错点,帮助你在编码和面试中避坑。
9.1 --end() 的特殊处理
end() 是 nullptr,对它执行 -- 时,_node == nullptr 分支会触发,从 _root 出发找最右节点。这就是迭代器必须保存 _root 指针的原因。
易错点 :如果迭代器不保存
_root,--end()就无法实现。SGI-STL 用 header 哨兵位避免了这个问题,但我们的 nullptr 方案必须存根指针。
9.2 依赖模板参数的类型必须加 typename
在 set/map 中定义迭代器类型时:
cpp
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
RBTree<K, const K, SetKeyOfT>::Iterator 是依赖模板参数的类型,编译器在解析时不知道它是个类型,所以必须加 typename 显式声明,否则编译报错。
9.3 红黑树 vs AVL 树:为什么 STL 选红黑树?
| 对比项 | AVL 树 | 红黑树 |
|---|---|---|
| 平衡标准 | 左右子树高度差 ≤ 1 | 最长路径 ≤ 2 × 最短路径 |
| 插入调整 | 最多需要 O(log n) 次旋转 | 最多 2 次旋转 + 变色 |
| 删除调整 | 较复杂 | 相对简单 |
| 查找效率 | 略高(更严格平衡) | 略低但仍是 O(log n) |
| 插入/删除效率 | 较低(旋转次数多) | 较高(变色为主,旋转少) |
| 适用场景 | 查找密集型 | 插入删除频繁型 |
STL 选择红黑树的原因:实际应用中插入删除和查找是混合的,红黑树在插入删除时性能更优(变色是 O(1) 操作,旋转次数有上限),而查找性能损失很小(高度差不超过 2 倍)。综合来看红黑树是更均衡的选择。
9.4 迭代器失效问题
红黑树的插入和删除操作不会导致其他节点的迭代器失效 (只有被删除的节点迭代器失效)。这是因为红黑树的节点在堆上独立分配,旋转只改变指针指向,不移动节点内存。这和 vector 的迭代器失效有本质区别。
十、完整源码汇总
10.1 RBTree.h
cpp
#pragma once
enum Colour {
RED,
BLACK
};
template<class T>
struct RBTreeNode {
T _data;
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Colour _col;
RBTreeNode(const T& data)
: _data(data)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> Self;
Node* _node;
Node* _root;
RBTreeIterator(Node* node, Node* root)
: _node(node), _root(root)
{}
Self& operator++() {
if (_node->_right) {
Node* min = _node->_right;
while (min->_left)
min = min->_left;
_node = min;
} else {
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right) {
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
Self& operator--() {
if (_node == nullptr) { // --end()
Node* rightMost = _root;
while (rightMost && rightMost->_right)
rightMost = rightMost->_right;
_node = rightMost;
} else 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;
}
return *this;
}
Ref operator*() { return _node->_data; }
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; }
};
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;
Iterator Begin() {
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return Iterator(leftMost, _root);
}
Iterator End() {
return Iterator(nullptr, _root);
}
ConstIterator Begin() const {
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return ConstIterator(leftMost, _root);
}
ConstIterator End() const {
return ConstIterator(nullptr, _root);
}
RBTree() = default;
~RBTree() {
Destroy(_root);
_root = nullptr;
}
pair<Iterator, bool> Insert(const T& data) {
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK;
return { Iterator(_root, _root), true };
}
KeyOfT kot;
Node* parent = nullptr;
Node* cur = _root;
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 {
return { Iterator(cur, _root), false };
}
}
cur = new Node(data);
Node* newnode = cur;
cur->_col = RED;
if (kot(parent->_data) < kot(data))
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
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 {
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
parent->_col = 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 { Iterator(newnode, _root), 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, _root);
}
return End();
}
private:
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL) subRL->_parent = parent;
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr) {
_root = subR;
subR->_parent = nullptr;
} else {
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
}
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
Node* pParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
} else {
if (pParent->_left == parent)
pParent->_left = subL;
else
pParent->_right = subL;
subL->_parent = pParent;
}
}
void Destroy(Node* root) {
if (root == nullptr) return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
private:
Node* _root = nullptr;
};
10.2 my_set.h
cpp
#pragma once
#include "RBTree.h"
namespace bit {
template<class K>
class set {
struct SetKeyOfT {
const K& operator()(const K& key) { return key; }
};
public:
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
const_iterator begin() const { return _t.Begin(); }
const_iterator end() const { return _t.End(); }
pair<iterator, bool> insert(const K& key) {
return _t.Insert(key);
}
iterator find(const K& key) {
return _t.Find(key);
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
}
10.3 my_map.h
cpp
#pragma once
#include "RBTree.h"
namespace bit {
template<class K, class V>
class map {
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) { return kv.first; }
};
public:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
const_iterator begin() const { return _t.Begin(); }
const_iterator end() const { return _t.End(); }
pair<iterator, bool> insert(const pair<K, V>& kv) {
return _t.Insert(kv);
}
iterator find(const K& key) {
return _t.Find(key);
}
V& operator[](const K& key) {
pair<iterator, bool> ret = insert({ key, V() });
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
10.4 测试代码
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;
#include "my_set.h"
#include "my_map.h"
void test_set() {
bit::set<int> s;
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a) s.insert(e);
// 正向遍历(中序:升序)
for (auto e : s) cout << e << " ";
cout << endl;
// 反向遍历
auto it = s.end();
while (it != s.begin()) {
--it;
cout << *it << " ";
}
cout << endl;
}
void test_map() {
bit::map<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });
dict["left"] = "左边,剩余"; // key 存在,修改 value
dict["insert"] = "插入"; // key 不存在,插入
dict["string"]; // key 不存在,插入默认空串
for (auto& kv : dict) {
cout << kv.first << ":" << kv.second << endl;
}
}
int main() {
test_set();
test_map();
return 0;
}
十一、总结
本文从 SGI-STL 源码出发,完整实现了一颗泛型红黑树,并基于它封装了 my_map 和 my_set。核心知识点回顾:
- 泛型设计 :通过
Value模板参数控制节点存储类型,通过KeyOfT仿函数提取 key,一颗树同时服务 map 和 set; - 迭代器 :封装节点指针,
++/--基于中序遍历的局部逻辑,nullptr充当end(),--end()需特殊处理; - 插入调整:新节点默认红色,父亲为红时进入调整循环------叔叔红则变色向上传播,叔叔黑则旋转(单旋/双旋)后 break;
- 旋转:左旋转五步拆解,核心是指针交接和父指针同步更新;
- key 不可修改 :set 用
const K,map 用pair<const K, V>,从类型层面保证 key 不可变; - operator\[\]:基于 insert 返回值实现,有副作用,查询应用 find();
- const_iterator :通过
Ref/Ptr模板参数一套代码支持两种迭代器。
红黑树封装 map/set 是 C++ 进阶和数据结构面试的高频考点,理解了这套设计,再去看 STL 源码或其他平衡树(如 AVL、B 树)都会触类旁通。