引言
C++ 标准库中的 std::map 和 std::set 底层均基于红黑树 实现,但两者存储的数据类型不同:set 仅存储关键字 key,而 map 存储键值对 pair<const Key, T>。在 SGI-STL 源码中,map 和 set 并没有各自独立实现一棵红黑树,而是通过复用同一棵红黑树模板,利用模板参数和仿函数来区分两种不同的语义。这种设计体现了泛型编程的精妙:用一份红黑树代码同时支持 key 模型和 key/value 模型。
本文将以 SGI-STL 源码框架为参考,从零开始模拟实现 bit::map 和 bit::set。我们将重点分析:
-
红黑树如何通过模板参数
T统一存储不同类型的数据; -
如何利用
KeyOfT仿函数从T中提取比较所需的key; -
迭代器的实现,特别是
operator++和operator--的中序逻辑; -
map中operator[]如何基于insert返回值实现; -
如何控制
set的迭代器不支持修改,以及map的迭代器不允许修改key但允许修改value。
目录
[一、源码框架分析:SGI-STL 的设计思路](#一、源码框架分析:SGI-STL 的设计思路)
[2.1 红黑树节点](#2.1 红黑树节点)
[2.2 红黑树模板声明](#2.2 红黑树模板声明)
[2.3 set 的封装](#2.3 set 的封装)
[2.4 map 的封装](#2.4 map 的封装)
[3.1 迭代器的结构](#3.1 迭代器的结构)
[3.2 operator++ 的逻辑](#3.2 operator++ 的逻辑)
[3.3 operator-- 的逻辑](#3.3 operator-- 的逻辑)
[3.4 begin() 与 end()](#3.4 begin() 与 end())
[3.5 set 和 map 中的迭代器类型](#3.5 set 和 map 中的迭代器类型)
[四、map 的 operator[] 实现](#四、map 的 operator[] 实现)
[6.1 测试 set](#6.1 测试 set)
[6.2 测试 map 与 operator[]](#6.2 测试 map 与 operator[])
一、源码框架分析:SGI-STL 的设计思路
SGI-STL 中,set 和 map 并没有直接包含红黑树的具体实现,而是通过一个名为 rb_tree 的模板类来完成所有底层操作。rb_tree 的声明如下(简化):
cpp
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc>
class rb_tree {
// ...
};
-
Key:关键字类型(用于find、erase等接口的参数)。 -
Value:红黑树节点中实际存储的数据类型。 -
KeyOfValue:一个仿函数,用于从Value中提取Key。
对于 set,Value 就是 Key,KeyOfValue 通常是一个恒等仿函数(identity)。对于 map,Value 是 pair<const Key, T>,KeyOfValue 是一个提取 pair::first 的仿函数(select1st)。
rb_tree 提供了 insert_unique(不允许重复 key)和 insert_equal(允许重复 key)等接口。set 和 map 只使用 insert_unique,而 multiset 和 multimap 则使用 insert_equal。
这种设计使得红黑树完全独立于具体的应用场景,只要外界提供合适的 KeyOfValue 仿函数即可。
二、模拟实现的基本框架
我们将按照以下层次构建:
-
红黑树节点 :模板参数
T表示节点中存储的数据类型。 -
红黑树类 :模板参数
K(关键字类型)、T(存储的数据类型)、KeyOfT(提取关键字的仿函数)。 -
set类 :封装红黑树,T = K,KeyOfT返回K本身。 -
map类 :封装红黑树,T = pair<const K, V>,KeyOfT返回pair.first。
2.1 红黑树节点
节点中除了左右孩子和父节点指针外,还包含颜色和存储的数据 _data。
cpp
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)
, _col(RED)
{}
};
2.2 红黑树模板声明
红黑树需要知道如何从 T 中取出 key 进行比较,因此引入 KeyOfT 仿函数:
cpp
template<class K, class T, class KeyOfT>
class RBTree {
typedef RBTreeNode<T> Node;
public:
// 插入等接口
private:
Node* _root = nullptr;
};
K 是关键字类型,主要用于 find、erase 等接口的参数类型;T 是节点存储的数据类型;KeyOfT 是一个仿函数类,必须提供 operator() 将 T 转换为 K。
2.3 set 的封装
set 存储的是 K,因此 T = K。KeyOfT 只需要原样返回传入的 key。
cpp
template<class K>
class set {
struct SetKeyOfT {
const K& operator()(const K& key) const { return key; }
};
public:
bool insert(const K& key) {
return _t.Insert(key).second;
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
注意:set 不允许修改关键字,因此红黑树的 T 类型使用 const K,这样迭代器解引用后得到 const K&,无法修改。
2.4 map 的封装
map 存储的是 pair<const K, V>,因此 T = pair<const K, V>。KeyOfT 提取 pair.first。
cpp
template<class K, class V>
class map {
struct MapKeyOfT {
const K& operator()(const pair<const K, V>& kv) const {
return kv.first;
}
};
public:
bool insert(const pair<const K, V>& kv) {
return _t.Insert(kv).second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
此时红黑树中存储的数据类型是 pair<const K, V>,key 不可修改,value 可以修改。
三、迭代器的实现
迭代器是 STL 的核心组件,它封装了红黑树的节点指针,并提供 operator++、operator--、operator*、operator-> 等操作,使得用户可以像使用指针一样遍历容器。
3.1 迭代器的结构
迭代器需要知道当前节点指针,以及树的根节点(用于处理 end() 的特殊情况)。我们实现一个模板类 RBTreeIterator,支持 Ref 和 Ptr 来区分普通迭代器和 const 迭代器。
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*() const { return _node->_data; }
Ptr operator->() const { return &_node->_data; }
Self& operator++();
Self& operator--();
bool operator==(const Self& s) const { return _node == s._node; }
bool operator!=(const Self& s) const { return _node != s._node; }
};
3.2 operator++ 的逻辑
红黑树迭代器的 ++ 操作需要移动到中序遍历的下一个节点。中序遍历顺序:左子树 → 根 → 右子树。
情况1 :当前节点的右子树不为空
下一个节点是右子树中的最左节点(即右子树中中序第一个节点)。
情况2 :当前节点的右子树为空
说明当前节点所在的子树已经遍历完毕,需要向上回溯,找到第一个当前节点是其父节点的左孩子 的祖先节点,该祖先节点就是下一个要访问的节点。如果一直回溯到根都没有找到(即当前节点是整棵树的最右节点),则下一个节点为 nullptr,即 end()。
cpp
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 = parent->_parent;
}
_node = parent;
}
return *this;
}
3.3 operator-- 的逻辑
-- 操作是中序的逆过程,顺序为:右子树 → 根 → 左子树。
-
如果当前节点为
nullptr(即end()),则--应指向中序遍历的最后一个节点,即整棵树的最右节点。 -
如果当前节点的左子树不为空,则前驱节点是左子树中的最右节点。
-
否则,向上回溯,找到第一个当前节点是其父节点的右孩子的祖先节点。
cpp
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 = parent->_parent;
}
_node = parent;
}
return *this;
}
3.4 begin() 与 end()
-
begin()返回中序第一个节点,即树中最左节点。 -
end()返回nullptr迭代器(也可以像 SGI-STL 那样设计一个哨兵头节点,但nullptr更简单)。
cpp
Iterator Begin() {
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return Iterator(leftMost, _root);
}
Iterator End() {
return Iterator(nullptr, _root);
}
3.5 set 和 map 中的迭代器类型
-
set的迭代器:RBTree<K, const K, SetKeyOfT>::Iterator。由于T是const K,解引用得到const K&,不能修改。 -
map的迭代器:RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator。解引用得到pair<const K, V>&,first不可修改,second可修改。
在 set 和 map 中分别定义 iterator 和 const_iterator 类型,并实现 begin()/end() 的 const 版本。
四、map 的 operator[] 实现
map 的 operator[] 是一个非常便利的接口,它支持:
-
插入:若
key不存在,则插入pair(key, V())。 -
查找:若
key存在,则返回对应value的引用。 -
修改:通过返回的引用可以直接修改
value。
要支持 [],首先需要修改红黑树的 Insert 接口,使其返回 pair<Iterator, bool>,其中 Iterator 指向插入成功或已存在元素的迭代器。
cpp
pair<Iterator, bool> Insert(const T& data) {
// ... 插入逻辑,成功或失败时返回对应的迭代器和 bool
}
然后在 map 中实现 operator[]:
cpp
V& operator[](const K& key) {
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
如果 key 不存在,insert 会插入 {key, V()},V() 是值类型的默认构造(例如 int 为 0,string 为空串),然后返回迭代器指向该新节点,并返回 second = true。如果 key 已存在,insert 返回的迭代器指向已有节点,second = false。无论哪种情况,ret.first->second 都是 value 的引用,可以用于读取或赋值。
五、完整的封装示例
以下给出 RBTree 中 Insert 返回 pair<Iterator, bool> 的关键修改(插入逻辑与之前红黑树相同,只是返回值类型变化):
cpp
pair<Iterator, bool> Insert(const T& data) {
if (_root == nullptr) {
_root = new Node(data);
_root->_col = BLACK;
return make_pair(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 make_pair(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;
// ... 后续的平衡处理(变色和旋转)与之前相同 ...
_root->_col = BLACK;
return make_pair(Iterator(newnode, _root), true);
}
set 和 map 的 insert 只需调用 _t.Insert 并返回对应的 pair。
六、测试与验证
6.1 测试 set
cpp
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;
}
6.2 测试 map 与 operator[]
cpp
void test_map() {
bit::map<string, string> dict;
dict.insert({"sort", "排序"});
dict.insert({"left", "左边"});
dict["left"] = "左边,剩余";
dict["insert"] = "插入";
dict["string"]; // 插入 {"string", ""}
for (auto& kv : dict) {
cout << kv.first << ":" << kv.second << endl;
}
}
总结
本文基于 SGI-STL 的源码设计思想,使用同一棵红黑树模板实现了 bit::set 和 bit::map。关键点包括:
-
泛型数据存储 :红黑树节点存储模板参数
T,通过KeyOfT仿函数提取比较所需的key,使得红黑树与具体的数据类型解耦。 -
迭代器实现 :红黑树迭代器按照中序遍历顺序移动,
operator++和operator--分别处理右子树非空和回溯祖先两种情况,begin()返回最左节点,end()返回nullptr。 -
set和map的差异化 :set将T实例化为const K,禁止修改;map将T实例化为pair<const K, V>,允许修改value但不允许修改key。 -
map的operator[]:基于insert返回的迭代器,直接获取value的引用,集插入、查找、修改于一体。
通过这次模拟实现,我们不仅加深了对红黑树底层机制的理解,也掌握了泛型容器封装的高级技巧。这种复用同一底层结构、通过模板参数和仿函数定制行为的方式,是 C++ 泛型编程的经典范例,也是阅读 STL 源码的重要基础。