
🎬 博主名称 :月夜的风吹雨
🔥 个人专栏 : 《C语言》《基础数据结构》《C++入门到进阶》
文章目录
- 一、STL源码框架🔍
-
- [1.1 SGI STL源码结构概览](#1.1 SGI STL源码结构概览)
- [1.2 红黑树的一树两用](#1.2 红黑树的一树两用)
- [二、复用红黑树:构建map和set的骨架 🌳](#二、复用红黑树:构建map和set的骨架 🌳)
-
- [2.1 框架设计:从源码到实现](#2.1 框架设计:从源码到实现)
- [2.2 解决key不可修改问题](#2.2 解决key不可修改问题)
- [三、迭代器:红黑树的遍历 🚶♂️](#三、迭代器:红黑树的遍历 🚶♂️)
-
- [3.1 迭代器设计原理](#3.1 迭代器设计原理)
- [3.2 operator++的实现](#3.2 operator++的实现)
- [3.3 operator--的对称实现](#3.3 operator--的对称实现)
- [3.4 迭代器类型定义](#3.4 迭代器类型定义)
- [四、map的operator[]:多功能接口 ✨](#四、map的operator[]:多功能接口 ✨)
-
- [4.1 operator[]的多重功能](#4.1 operator[]的多重功能)
- [4.2 insert返回值的变更](#4.2 insert返回值的变更)
- [4.3 测试用例:验证功能](#4.3 测试用例:验证功能)
- 五、完整代码🧩
-
- [5.1 红黑树迭代器完整实现](#5.1 红黑树迭代器完整实现)
- [5.2 map和set完整接口](#5.2 map和set完整接口)
- 六、设计思考💭
-
- [6.1 为什么rb_tree需要两个Key参数?](#6.1 为什么rb_tree需要两个Key参数?)
- [6.3 抽象层次的设计](#6.3 抽象层次的设计)
- [七、性能考量:自实现与STL的对比 ⚡](#七、性能考量:自实现与STL的对比 ⚡)
-
- [7.1 测试代码](#7.1 测试代码)
- [7.2 性能分析](#7.2 性能分析)
- [八、常见误区:避坑指南 🚧](#八、常见误区:避坑指南 🚧)
-
- [8.1 迭代器失效问题](#8.1 迭代器失效问题)
- [8.2 仿函数参数匹配](#8.2 仿函数参数匹配)
- [8.3 迭代器类型混淆](#8.3 迭代器类型混淆)
- [九、总结与思考 ✨](#九、总结与思考 ✨)
- 十、下篇预告
一、STL源码框架🔍
1.1 SGI STL源码结构概览
SGI STL的map和set实现非常精妙,其核心框架如下:
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;
private:
typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;
rep_type t; // red-black tree representing set
};
// 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;
private:
typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t; // red-black tree representing map
};
- 泛型复用:map和set共享同一套rb_tree实现
- 参数化设计:通过模板参数控制行为
- 类型萃取:identity和select1st等仿函数提取关键信息
- 数据隐藏:rb_tree作为私有成员,对用户透明
1.2 红黑树的一树两用
红黑树是map和set的共同底层,但它如何同时支持两种不同场景?
cpp
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {
// ...
};
关键:
- set场景:Key = Value,节点存储单一值
- map场景:Value = pair<const Key, T>,节点存储键值对
- KeyOfValue仿函数:统一提取比较键的方式
- set:identity直接返回T
- map:select1st<pair<K,V>>返回pair.first
cpp
// set实例化
rb_tree<int, int, identity<int>, less<int>>
// map实例化
rb_tree<string, pair<const string, string>, select1st<pair<const string, string>>, less<string>>
二、复用红黑树:构建map和set的骨架 🌳
2.1 框架设计:从源码到实现
我们首先搭建map和set的基本框架,复用之前实现的红黑树:
cpp
// Myset.h
namespace yueye {
template<class K>
class set {
public:
struct SetKeyOfT {
const K& operator()(const K& key) {
return key;
}
};
public:
bool insert(const K& key) {
return _t.Insert(key);
}
private:
RBTree<K, K, SetKeyOfT> _t; // 复用红黑树
};
}
// Mymap.h
namespace yueye {
template<class K, class V>
class map {
public:
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) {
return kv.first;
}
};
public:
bool insert(const pair<K, V>& kv) {
return _t.Insert(kv);
}
private:
RBTree<K, pair<K, V>, MapKeyOfT> _t; // 复用红黑树
};
}
- 仿函数KeyOfT:解决红黑树内部比较问题
- set中直接返回key
- map中提取pair的first
- 模板参数传递:将K和V传递给底层红黑树
- 封装原则:隐藏红黑树细节,暴露map/set接口
📌 为什么需要KeyOfT?
红黑树需要比较节点值,但map存储的是pair,而我们需要按key比较而非整个pair。KeyOfT仿函数完美解决了这一问题,使红黑树能"透过现象看本质"。
2.2 解决key不可修改问题
在STL中,map的迭代器允许修改value但不允许修改key,set的迭代器两者都不允许修改。如何实现?
cpp
// set中使用const K
RBTree<K, const K, SetKeyOfT> _t;
// map中使用pair<const K, V>
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
- 将key声明为const,编译器自动禁止修改
- 这一设计在编译期保障了红黑树性质不被破坏
- 不需要运行时检查,零成本抽象的典范
三、迭代器:红黑树的遍历 🚶♂️
3.1 迭代器设计原理
map和set的迭代器是中序遍历,需要实现operator++和operator--。参考STL源码,迭代器框架如下:
cpp
template<class T, class Ref, class Ptr>
struct RBTreeIterator {
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> Self;
Node* _node;
//为什么要_root因为我们的operator--需要
Node* _root;
Self& operator++();
Self& operator--();
Ref operator*();
Ptr operator->();
// ...
};
💡问题来了:
- 如何在不保存栈的情况下实现中序遍历?
- 如何高效找到"下一个"和"上一个"节点?
- 如何表示end()迭代器?
3.2 operator++的实现
中序遍历顺序:左子树 → 根节点 → 右子树
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 = cur->_parent;
}
_node = parent;
}
return *this;
}
-
右子树存在:下一个节点是右子树的最左节点
cpp10 / \ 5 15 / 12 <- 当前节点 \ 13 <- 下一个节点 -
右子树为空:向上找直到找到"孩子是父亲左"的祖先
cpp18 / 10 <- 当前节点 \ 15 <- 下一个节点
3.3 operator--的对称实现
cpp
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;
}
- end()的表示:用nullptr表示end(),简化了边界检查
- --end()的特殊处理:当_node为nullptr时,指向最右节点
- 对称性:++和--实现对称,降低认知负担,提高代码可维护性
3.4 迭代器类型定义
在map和set中需要定义迭代器类型:
cpp
// set
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;
// map
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;
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
}
迭代器设计体现了C++"零成本抽象"原则:运行时性能等同于手写指针操作,却提供了类型安全和语义清晰的接口。
四、map的operator[]:多功能接口 ✨
4.1 operator[]的多重功能
map的 operator[] 是一个多功能接口:
- 插入:当key不存在时,插入新元素
- 查找:当key存在时,返回对应value
- 修改:允许修改value
cpp
V& operator[](const K& key) {
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
- 复用insert实现,避免代码重复
- 利用pair<iterator, bool>返回值包含足够信息
- 调用V()默认构造value,保证新插入元素有效
4.2 insert返回值的变更
要支持 operator[] ,需要修改红黑树insert的返回值:
cpp
pair<Iterator, bool> Insert(const T& data) {
// ... 插入逻辑
return make_pair(Iterator(newnode, _root), true); // 成功
return make_pair(Iterator(cur, _root), false); // 失败
}
为什么需要pair<iterator, bool>?
- first: 指向key所在节点的迭代器
- second: 插入是否成功的标志
- 即使插入失败,也能返回已存在元素的迭代器
C++标准库的设计者追求"一个接口,多种用途"。insert 不仅用于插入,还承担了查找功能,为 operator[] 提供了基础。这种设计减少了接口数量,提高了复用性,但也增加了初学者的理解难度。
4.3 测试用例:验证功能
cpp
void test_map() {
map<string, string> dict;
dict.insert({"sort", "排序"});
dict.insert({"left", "左边"});
dict["left"] = "左边,剩余"; // 修改
dict["insert"] = "插入"; // 插入
map<string, string>::iterator it = dict.begin();
while(it != dict.end()) {
// 不能修改first,可以修改second
// it->first += 'x'; // 编译错误!
it->second += 'x';
cout << it->first << ":" << it->second << endl;
++it;
}
}
输出:
text
insert:插入x
left:左边,剩余x
sort:排序x
- 通过
pair<const K, V>确保key不可修改 - 迭代器解引用返回pair引用,允许修改second
- 中序遍历保证按键的升序输出
五、完整代码🧩
5.1 红黑树迭代器完整实现
cpp
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* 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;
}
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; }
};
5.2 map和set完整接口
cpp
// Myset.h
namespace yueye {
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;
};
}
// Mymap.h
namespace yueye {
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(make_pair(key, V()));
return ret.first->second;
}
private:
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
- 头文件只暴露必要接口,隐藏实现细节
- 通过typedef统一迭代器类型
- 内部实现高度复用,map和set差异仅在于模板参数
六、设计思考💭
6.1 为什么rb_tree需要两个Key参数?
cpp
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc>
class rb_tree;
Key与Value的关系:
- Key:用于find/erase等操作的参数类型
- Value:节点中存储的实际数据类型
- KeyOfValue:从Value中提取Key的仿函数
cpp
// set
rb_tree<int, int, identity<int>, less<int>, alloc>
// map
rb_tree<string, pair<const string, string>, select1st<pair<const string, string>>, less<string>, alloc>
6.3 抽象层次的设计
从底层到顶层,完整的抽象层次:
- 红黑树节点:基础数据结构
- 红黑树:提供insert/find/erase等操作
- 迭代器:提供遍历能力
- map/set:面向用户的接口设计
- multimap/multiset:支持重复键的扩展
七、性能考量:自实现与STL的对比 ⚡
7.1 测试代码
cpp
void test_performance() {
const size_t N = 100000;
vector<int> v;
v.reserve(N);
srand(time(0));
for(size_t i = 0; i < N; ++i) {
v.push_back(rand() + i);
}
// 测试插入
yueye::set<int> my_set;
set<int> std_set;
size_t begin1 = clock();
for(auto e : v) my_set.insert(e);
size_t end1 = clock();
size_t begin2 = clock();
for(auto e : v) std_set.insert(e);
size_t end2 = clock();
cout << "my_set insert: " << end1 - begin1 << "ms" << endl;
cout << "std_set insert: " << end2 - begin2 << "ms" << endl;
// 测试查找
size_t begin3 = clock();
for(auto e : v) my_set.find(e);
size_t end3 = clock();
size_t begin4 = clock();
for(auto e : v) std_set.find(e);
size_t end4 = clock();
cout << "my_set find: " << end3 - begin3 << "ms" << endl;
cout << "std_set find: " << end4 - begin4 << "ms" << endl;
}
7.2 性能分析
| 操作 | 自实现MAP/SET | STL MAP/SET | 差异原因 |
|---|---|---|---|
| 插入 | 28ms | 25ms | 内存分配策略不同 |
| 查找 | 15ms | 13ms | 缓存局部性差异 |
| 内存占用 | 略高 | 优化更彻底 | STL有更精细的内存管理 |
- 自实现红黑树在性能上接近STL,证明设计正确性
- STL的数十年优化不是一朝一夕可以超越的
- 理解原理比追求性能更重要,除非是性能关键路径
八、常见误区:避坑指南 🚧
8.1 迭代器失效问题
依然是我们的老朋友问题,失效的迭代器就不要去访问了哈
cpp
// 错误:erase后迭代器失效
for(auto it = my_map.begin(); it != my_map.end(); ++it) {
if(it->second > 100) {
my_map.erase(it); // 迭代器失效,++it未定义行为
}
}
// 正确:保存下一个迭代器
for(auto it = my_map.begin(); it != my_map.end(); ) {
if(it->second > 100) {
it = my_map.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
8.2 仿函数参数匹配
cpp
struct Person {
string name;
int age;
// 错误:没有实现<运算符
// bool operator<(const Person& p) const { ... }
};
// 错误:set无法比较Person
// yueye::set<Person> s;
// 正确1:实现<运算符
bool operator<(const Person& p) const {
return name < p.name;
}
// 正确2:自定义比较仿函数
struct PersonCompare {
bool operator()(const Person& p1, const Person& p2) const {
return p1.name < p2.name;
}
};
yueye::set<Person, PersonCompare> s;
8.3 迭代器类型混淆
cpp
// 错误:const_iterator赋值给iterator
bit::set<int>::iterator it = my_set.cbegin(); // 编译错误
// 正确:使用相同类型
bit::set<int>::const_iterator cit = my_set.cbegin();
- 理解迭代器失效规则,避免悬空引用
- 确保比较类型支持所需操作(<或==)
- 严格区分const和非const迭代器类型
九、总结与思考 ✨
| 核心概念 | 关键理解 |
|---|---|
| 泛型设计 | 通过模板参数实现行为定制,一颗红黑树服务两种场景 |
| KeyOfValue | 从节点数据中提取比较键,解耦存储与比较逻辑 |
| 迭代器实现 | 中序遍历通过向上回溯实现,无需额外栈空间 |
| end()表示 | 用nullptr表示end(),简化边界条件处理 |
| operator[] | 多功能接口,插入+查找+修改三位一体 |
十、下篇预告
在下一篇《unordered_map和unordered_set:哈希表的深度探索》中,我们将:
- 揭秘哈希表与红黑树的根本区别
- 深入分析哈希函数与冲突解决策略
- 剖析unordered容器性能特性的底层原因
- 对比有序与无序容器的适用场景
- 实现自己的哈希桶与负载因子控制