深入红黑树:从STL源码到亲手封装 map 和 set
引言
在学习 C++ 标准模板库时,map 和 set 是我们最常用的关联式容器。你是否想过,为什么 map 存储的是键值对,而 set 只存储键——但它们却共用同一套底层数据结构?为什么它们的迭代器都是有序的?
1. 源码框架层:一棵树如何撑起两种容器
1.1 核心矛盾
打开 SGI STL 源码,stl_map.h 和 stl_set.h 中都包含了 stl_tree.h。它们底层都依赖同一棵红黑树,但问题来了:
-
set存放的是单纯的Key -
map存放的是pair<const Key, T>
如何让同一棵树既存 Key 又存 pair?
1.2 泛型模板参数的艺术
观察源码中 rb_tree 的模板参数:
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree { ... };
关键在第二个模板参数 Value:
-
set 实例化时:
rb_tree<key_type, key_type, identity<value_type>, ...>-
第二个参数直接传
key_type,树节点存储的就是 Key 本身
-
-
map 实例化时:
rb_tree<key_type, pair<const Key, T>, select1st<value_type>, ...>-
第二个参数传
pair<const Key, T>,树节点存储键值对
-
这样,一棵红黑树通过模板参数 Value 控制了节点存储的真实数据类型,完美复用于两种场景。
1.3 三个模板参数的分工
| 模板参数 | set 中 | map 中 | 作用 |
|---|---|---|---|
| Key | Key | Key | erase / find 的形参类型 |
| Value | Key | pair<const Key, T> |
决定节点存什么数据 |
| KeyOfValue | identity<Key> |
select1st<pair> |
从节点数据中提取 Key |
为什么需要 KeyOfValue?
因为插入时需要比较大小,但 pair 默认的比较会先比 key 再比 value——而我们只希望按 key 比较。所以需要仿函数从 Value 中“取出”Key 来比较。
2. 动手实现: mymap 和 myset
2.1 调整模板命名
相比源码,我们做一点命名优化:
-
第一个参数用
K(对应源码的 Key) -
第二个参数用
V(源码中 map 的第二个参数叫 T,容易混淆) -
红黑树中的数据类型用
T
2.2 实现 KeyOfT 仿函数
Mymap.h:
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first; // 从 pair 中提取 key
}
};
public:
bool insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
private:
RBTree<K, pair<K, V>, MapKeyOfT> _t;
};
Myset.h:
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key; // set 的 key 就是自己
}
};
public:
bool insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree<K, K, SetKeyOfT> _t;
};
这样,红黑树内部通过 KeyOfT 仿函数统一提取 Key 进行比较,完全不关心 T 到底是什么类型。
3. 迭代器实现:中序遍历的巧妙抽象
3.1 大框架:封装节点指针
迭代器的实现思路与 list 的迭代器一致——封装一个节点指针,重载 *、->、++、-- 等运算符,使其像指针一样访问数据。
真正的难点是 operator++ 和 operator--。
3.2 operator++ 的逻辑
红黑树的迭代器按中序遍历(左子树 → 根 → 右子树)移动:
情况一:右子树不为空
当前节点访问完毕,下一个节点是右子树的最左节点。
情况二:右子树为空
说明当前子树全部访问完毕,需要沿着祖先路径向上找:
-
如果当前节点是父节点的左孩子,则下一个就是父节点
-
如果当前节点是父节点的右孩子,则继续往上找,直到找到孩子是父亲左孩子的那个祖先
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;
}
return *this;
}
3.3 end() 的设计
我们用 nullptr 充当 end()。当迭代器指向最右节点(最大节点)再 ++ 时,找不到”孩子是父亲左”的祖先,最终 parent 为空,_node 被置为 nullptr,即 end()。
--end() 需要特殊处理:
Self& operator--()
{
if (_node == nullptr) // end()
{
// 回到中序最后一个节点(整棵树的最右节点)
Node* rightMost = _root;
while (rightMost && rightMost->_right)
rightMost = rightMost->_right;
_node = rightMost;
}
// ... 后续逻辑与 ++ 对称
}
4. 进阶细节:const 支持和 operator[]
4.1 key 不可修改的问题
set:值就是 key,所以整个值不能修改。把 set 的第二个模板参数改为 const K:
RBTree<K, const K, SetKeyOfT> _t;
map:key 不能修改但 value 可以。把 pair 的第一个参数改为 const K:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
4.2 map 的 operator[]
改进的Insert
template<class Key, class T, class KeyOfT>
pair<typename RBTree<Key, T, KeyOfT>::Iterator,bool> RBTree<Key, T, KeyOfT>::Insert(const T& data)
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = Black;
return pair<Iterator, bool>({ Iterator(_root, _root),true });
}
Node* parent = nullptr;
Node* cur = _root;
KeyOfT koft;
while (cur)
{
if (koft(cur->_data) < koft(data))
{
parent = cur;
cur = cur->_right;
}
else if(koft(cur->_data) > koft(data))
{
parent = cur;
cur = cur->_left;
}else
{
return pair<Iterator, bool>({ Iterator(cur,_root),false});
}
}
cur = new Node(data);
if (koft(data) < koft(parent->_data))
{
parent->_left = cur;
}
else
{
parent->_right = 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;
}
else
{
_rotateL(parent);
_rotateR(grandfather);
cur->_col = Black;
}
grandfather->_col = Red;
break;
}
}
else
{
Node* uncle = parent->_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;
}
else
{
_rotateR(parent);
_rotateL(grandfather);
cur->_col = Black;
}
grandfather->_col = Red;
break;
}
}
}
_root->_col = Black;
return pair<Iterator, bool>({ Iterator(cur,_root),true });
}
有了改进后的 Insert(返回 pair<iterator, bool>),operator[] 实现极简:
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
如果 key 已存在,insert 返回已有节点的迭代器;不存在则插入 key: V() 并返回新节点迭代器。最终都返回对应 value 的引用。
5. 总结:泛型思想的胜利
通过封装红黑树实现 map 和 set,我们看到泛型编程的威力:
| 层次 | 职责 |
|---|---|
| RBTree | 数据结构核心:旋转、调整平衡、迭代器遍历 |
| KeyOfT | 策略层:从不同类型数据中提取 Key 用于比较 |
| mymap / myset | 适配层:向用户暴露接口,指定存储类型和只读策略 |
这种策略与实现分离的设计模式,使得同一棵红黑树可以服务多种容器,也是 STL 精髓所在。
SGI 源码中 rb_tree 的五个模板参数,每一个都有其存在意义。理解这个设计,对我们阅读 STL 源码、设计通用组件都大有裨益。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)