前言
上一篇文章中,我们用两个模板参数K和V实现了红黑树。但如果去翻翻 SGI-STL 的源码,会发现红黑树的模板参数长这样——rb_tree<Key, Value, KeyOfValue, Compare, Alloc>,五个参数,每个都有它的来头。为什么 set 和 map 共用同一棵红黑树?为什么 set 的 value_type 是 const K 而 map 是 pair<const K, V>?本文从 STL 源码的设计思路出发,一步步把一棵裸红黑树封装成可以直接拿来用的 set 和 map。
1. 从 STL 源码看设计思路
SGI-STL 是 Linux 下 GNU C++ 标准库的前身,map 和 set 的底层红黑树实现就是从这里来的。我们先看它怎么组织代码:
// stl_set.h —— set 的模板参数
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:
typedef Key key_type;
typedef Key value_type; // set 的 value 就是 key 本身
private:
typedef rb_tree<key_type, value_type,
identity<value_type>, // ← 关键:怎么从 value 里取 key
key_compare, Alloc> rep_type;
rep_type t;
};
// stl_map.h —— map 的模板参数
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 是键值对
private:
typedef rb_tree<key_type, value_type,
select1st<value_type>, // ← 关键:从 pair 里取 first
key_compare, Alloc> rep_type;
rep_type t;
};
两个类,共用同一套 rb_tree 模板,区别只在于传入的类型和那个提取 key 的仿函数。这个设计非常简洁,核心思想就是一句话:红黑树只负责按 key 排序,至于 key 怎么从 value 里拿出来,你传个仿函数告诉我就行。
2. 改造红黑树:从 pair<K,V> 到泛型 T
上一篇文章里我们实现的红黑树,节点存的是 pair<K, V>,比较时直接用 kv.first。这只能服务于一种场景。要让同一棵树既能存 K(set)又能存 pair<K,V>(map),第一步就是把节点的数据类型泛化。
2.1 节点结构
template<class T>
struct RBTreeNode
{
T _data; // 不再写死 pair,用泛型 T
RBTreeNode<T>* _left;
RBTreeNode<T>* _right;
RBTreeNode<T>* _parent;
Colour _col;
RBTreeNode(const T& data)
: _data(data)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
{}
};
2.2 红黑树模板参数
template<class K, class T, class KeyOfT>
class RBTree
{
// K : key 的类型,Find/Erase 的参数会用到
// T : 节点里存的数据类型(对于 set 是 K,对于 map 是 pair<const K, V>)
// KeyOfT : 仿函数,从 T 中提取 K
...
};
三个模板参数,各司其职:
| 参数 | set 传入 | map 传入 | 作用 |
|---|---|---|---|
K | int | string | 键的类型,Find(key) 的参数 |
T | const int | pair<const string, int> | 节点存储的数据类型 |
KeyOfT | SetKeyOfT | MapKeyOfT | 从 T 中提取 K 的仿函数 |
2.3 关键改进:用 KeyOfT 替代直接访问 .first
改造前的 Insert:
// 旧代码:写死了 pair 的 first 作为 key
if (kv.first < cur->_kv.first) { ... }
改造后:
// 新代码:通过 KeyOfT 提取 key,不关心 T 到底是什么类型
KeyOfT kot;
if (kot(data) < kot(cur->_data)) { ... }
这一层间接性就是封装的精髓——红黑树不需要知道 T 是 pair 还是 int,它只需要知道怎么从 T 中拿到 key。增删查改的逻辑完全不用动,只换了一个取值方式。
3. 迭代器:让红黑树支持范围 for
没有迭代器的容器只能手动 Find,有了迭代器才能用 for (auto e : s) 这种写法,才算一个合格的 STL 风格容器。
3.1 迭代器的前进逻辑(operator++)
中序遍历的下一个节点怎么找?分两种情况:
情况一:当前节点有右子树
→ 下一个节点 = 右子树的最左节点
cur
/ \
... (右子树)
/
(一路向左)
/
答案 ←
情况二:当前节点没有右子树
→ 沿着父指针向上走,直到找到一个祖先,当前节点在这个祖先的左子树中
答案
\
...
/
parent
\
cur (cur 在 parent 的右边,继续往上)
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.2 迭代器的后退逻辑(operator--)
operator-- 是 operator++ 的镜像,但有一步特殊处理——处理 end():
Self& operator--()
{
if (_node == nullptr) // 当前指向 end()
{
// --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;
}
这也是为什么迭代器内部要存一个
_root指针——在处理--end()时需要回到树里找最大节点。
3.3 迭代器为什么要保留 _root
完整迭代器结构:
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
typedef RBTreeNode<T> Node;
Node* _node; // 当前指向的节点
Node* _root; // 整棵树的根(operator-- 从 end() 回去时会用到)
...
};
_root 只有一个用途:当迭代器指向 end()(即 nullptr)时,执行 --it 需要找到整棵树的最右节点,而迭代器自身没有别的办法拿到树的根,只能靠构造时就存好。
SGI-STL 的做法更巧妙:它用一个哨兵节点
header来同时充当end()和根节点的父节点,header->left指向最左,header->right指向最右,--end()直接取header->right。我们的实现简化了这个设计,直接在迭代器里记一个_root指针,逻辑更直观。
3.4 const 与非 const 迭代器
Ref 和 Ptr 这两个模板参数的作用就是区分 iterator 和 const_iterator:
// RBTree 中定义
typedef RBTreeIterator<T, T&, T*> Iterator; // Ref=T&, Ptr=T*
typedef RBTreeIterator<T, const T&, const T*> ConstIterator; // Ref=const T&, Ptr=const T*
这样 operator* 和 operator-> 的返回值自动适配:
Ref operator*() { return _node->_data; } // Iterator → T&, ConstIterator → const T&
Ptr operator->() { return &_node->_data; } // Iterator → T*, ConstIterator → const T*
4. 改造 Insert:返回 pair<iterator, bool>
STL 中 insert 的返回值是一个 pair<iterator, bool>,iterator 指向插入元素(无论成功还是失败),bool 表示是否插入成功:
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(data) < kot(cur->_data))
{
parent = cur;
cur = cur->_left;
}
else if (kot(data) > kot(cur->_data))
{
parent = cur;
cur = cur->_right;
}
else
{
return { Iterator(cur, _root), false }; // key 已存在,返回已存在元素迭代器 + false
}
}
cur = new Node(data);
Node* newnode = cur; // 保存下来,最后返回
cur->_col = RED;
// ... 挂载到父节点,红黑树调整(不变)...
_root->_col = BLACK;
return { Iterator(newnode, _root), true }; // 返回新节点迭代器 + true
}
注意 newnode 变量的作用:红黑树调整过程中 cur 可能会被改成别的节点(情况一的向上回溯),所以必须在一开始就保存新节点的指针,最后返回时用它。
5. 封装 set:key 即 value
有了迭代器和改造后的 Insert,封装 set 就很简单了——本质上就是一个只暴露了 key 相关接口的壳:
namespace jys
{
template<class K>
class set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key; // set 的 value 就是 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();
}
pair<iterator, bool> insert(const K& key)
{
return _t.Insert(key);
}
private:
RBTree<K, const K, SetKeyOfT> _t;
};
}
值得注意的细节:RBTree 的第二个模板参数传的是 const K 而不是 K。这是因为 set 的 key 不能被修改——如果允许通过迭代器改 key,整棵树的排序就乱掉了。const K 让 Iterator 的 operator* 返回 const K&,编译期就杜绝了非法修改。
6. 封装 map:key 和 value 分离
map 的封装略复杂一些,因为存储的是键值对,并且需要支持 operator[]:
namespace jys
{
template<class K, class V>
class map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first; // 从 pair 中提取 key(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();
}
pair<iterator, bool> insert(const pair<K, V>& data)
{
return _t.Insert(data);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second; // iterator 指向 pair<const K, V>,取 second
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
6.1 为什么是 pair<const K, V> 而不是 pair<K, V>?
和 set 一样的原因——防止 key 被修改。map 的 key 同样是排序依据,不能动。传入 const K 作为 first 后,迭代器的 operator* 返回 pair<const K, V>&,first 不可改,second 可改:
it->first = new_key; // ❌ 编译错误
it->second = new_value; // ✅ 正确
6.2 operator[] 的实现
operator[] 是 map 最常用的接口之一,它的语义是:
- 如果 key 存在,返回对应 value 的引用
- 如果 key 不存在,插入
{key, V()}并返回新插入 value 的引用
这在红黑树 Insert 返回 pair<iterator, bool> 的基础上,两行就能搞定:
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
ret.first 是迭代器,指向 pair<const K, V>;ret.first->second 是 V&。无论 key 之前存不存在,这个引用都指向正确的 value。
7. 测试:验证整个体系
void test_set()
{
jys::set<int> s;
s.insert(1);
s.insert(6);
s.insert(3);
s.insert(8);
s.insert(9);
jys::set<int>::iterator sit = s.begin();
while (sit != s.end())
{
cout << *sit << " "; // 输出:1 3 6 8 9(中序有序)
++sit;
}
cout << endl;
}
void test_map()
{
jys::map<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });
dict["left"] = "左边(剩余)"; // 修改已存在的
dict["insert"] = "插入"; // 插入新的(key 不存在)
dict["string"]; // 插入空的(value 用默认构造)
jys::map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
// it->first = "xxx"; // ❌ 编译错误,key 不可修改
it->second += "x"; // ✅ value 可以修改
cout << it->first << ":" << it->second << endl;
++it;
}
// 范围 for —— 同样是靠迭代器支持
for (auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
}
8. 总结
回顾整个封装过程,核心思路其实只有一条:把红黑树中"依赖具体类型"的部分抽出来,交给模板参数和仿函数去适配。
第一步,把节点数据类型从写死的 pair<K, V> 泛化为模板参数 T。这步做完,同一棵树就能同时存储 int(给 set 用)和 pair<const K, V>(给 map 用)。第二步,用 KeyOfT 仿函数替代直接访问 .first。红黑树的插入、查找逻辑只跟 key 的比较有关,至于 key 是怎么从 T 里取出来的——set 直接返回自身,map 返回 pair.first——树本身不需要关心。第三步,给红黑树配上迭代器,让 operator++ / operator-- 走中序遍历的顺序,Begin() 返回最左节点,End() 返回 nullptr 作为哨兵,operator-- 从 end() 回到最大值节点时靠迭代器里存的 _root 找到树的最右节点。这套迭代器到位后,范围 for、find、operator[] 等上层接口就能自然地搭建出来。第四步,Insert 返回值从 bool 改为 pair<iterator, bool>——这是支撑 map::operator[] 的关键,[] 本质上就是调用 insert,然后从返回的迭代器里取出 second。
经过这四步,红黑树还是那棵红黑树,旋转和变色的逻辑也无需修改,但 set 和 map 两种接口风格截然不同的容器已经架在了同一套底层代码之上。树负责按 key 排序,仿函数负责从 value 中提取 key,容器负责对外暴露接口——三者层层封装,职能明确,这就是STL封装的艺术。
如有纰漏,敬请指正
6254



被折叠的 条评论
为什么被折叠?



