C++进阶:(八)基于红黑树泛型封装实现 map 与 set 容器
目录
前言
一、STL 源码框架分析
1.1 核心头文件依赖关系
1.2 关键类模板定义
1.2.1 set 的类模板结构
1.2.2 map 的类模板结构
1.2.3 红黑树(rb_tree)的核心模板参数
1.3 核心设计思想:泛型复用
1.3.1 节点存储类型的灵活控制
1.3.2 key 提取机制:KeyOfValue 仿函数
1.3.3 模板参数 Key 的作用
1.4 关于源码设计的小吐槽
二、模拟实现的核心思路
2.1 关键调整与优化
三、完整代码实现
3.1 红黑树的实现(RBTree.h)
3.2 set 的实现(Myset.h)
3.3 map 的实现(Mymap.h)
四、核心细节解析
4.1 红黑树的平衡规则与调整
4.2 迭代器的实现原理
4.2.1 operator++(中序下一个节点)
4.2.2 operator--(中序前一个节点)
4.3 key 不可修改的实现
4.4 map 的 operator [] 实现
总结
前言
在 C++ STL 中,map 和 set 是常用的关联式容器,它们的底层实现都依赖于红黑树这一高效的数据结构。红黑树作为一种自平衡二叉搜索树,既保证了查找、插入、删除操作的时间复杂度为 O (log n),又通过特定的规则维持树的平衡,避免了普通二叉搜索树在极端情况下退化为链表的问题。本文将从 STL 源码框架分析入手,详细讲解如何基于红黑树封装实现 map 和 set 容器,帮助大家深入理解关联式容器的底层机制。下面就让我们正式开始吧!
一、STL 源码框架分析
要实现 map 和 set,首先需要理解 STL 中 map 和 set 的底层实现逻辑。SGI-STL3.0 版本中,map 和 set 的源代码主要分布在stl_map.h、stl_set.h和stl_tree.h三个头文件中,其核心设计思想是通过红黑树的泛型实现,同时支撑 map(key-value 映射)和 set(key 集合)两种场景。
1.1 核心头文件依赖关系
STL 中 map 和 set 的实现高度依赖红黑树(rb_tree),其头文件包含关系如下:
- set 的实现依赖
stl_tree.h、stl_set.h和stl_multiset.h - map 的实现依赖
stl_tree.h、stl_map.h和stl_multimap.h
这种依赖关系的本质是:set 和 map 本身不直接实现数据结构,而是通过封装红黑树,对外提供特定的接口,形成符合自身语义的容器。
1.2 关键类模板定义
1.2.1 set 的类模板结构
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:// 红黑树类型定义,作为set的内部存储容器typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // 红黑树对象,真正存储数据
};
1.2.2 map 的类模板结构
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:typedef Key key_type;typedef T mapped_type; // map的value(映射值)类型typedef pair<const Key, T> value_type; // 红黑树存储的元素类型(key-value对)
private:// 红黑树类型定义,作为map的内部存储容器typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;rep_type t; // 红黑树对象,真正存储数据
};
1.2.3 红黑树(rb_tree)的核心模板参数
红黑树的类模板定义是实现 map 和 set 复用的关键,其核心模板参数如下:
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {
protected:typedef __rb_tree_node<Value> rb_tree_node; // 红黑树节点类型typedef rb_tree_node* link_type;size_type node_count; // 树中节点个数link_type header; // 头节点(哨兵节点)
public:// 插入唯一元素(不允许重复key)pair<iterator, bool> insert_unique(const value_type& x);// 根据key删除元素size_type erase(const key_type& x);// 根据key查找元素iterator find(const key_type& x);
};
1.3 核心设计思想:泛型复用
STL 通过红黑树的泛型设计,实现了 map 和 set 的复用,其关键在于以下两点:
1.3.1 节点存储类型的灵活控制
红黑树的第二个模板参数Value指定了节点中存储的数据类型:
- set 实例化红黑树时,
Value传入Key(即 set 的 value_type 就是 key),因此红黑树节点存储的是单个 key 值。 - map 实例化红黑树时,
Value传入pair<const Key, T>,因此红黑树节点存储的是 key-value 键值对。
通过这种方式,一颗红黑树既可以作为 set 的底层存储(存储单个 key),也可以作为 map 的底层存储(存储 key-value 对),实现了数据结构的复用。
1.3.2 key 提取机制:KeyOfValue 仿函数
红黑树的查找、插入、删除操作都需要基于 key 进行比较,但节点存储的是Value类型(可能是 key,也可能是 key-value 对)。为了从Value中提取出 key,红黑树引入了第三个模板参数KeyOfValue—— 一个仿函数,用于从Value对象中获取 key 值。
- set 对应的
KeyOfValue是identity<value_type>,直接返回Value本身(因为 set 的Value就是 key)。 - map 对应的
KeyOfValue是select1st<value_type>,返回pair<const Key, T>中的第一个元素(即 key)。
1.3.3 模板参数 Key 的作用
红黑树的第一个模板参数Key是查找、删除操作的参数类型:
- 对于 set,
Key与Value类型相同,查找(find)、删除(erase)时直接传入 key 即可。 - 对于 map,
Key是 key 的类型,而Value是pair<const Key, T>,查找、删除时传入 key,红黑树通过KeyOfValue仿函数从节点的Value中提取 key 进行比较。
这一设计解决了 map 中 “插入元素是 key-value 对,而查找删除是 key” 的场景差异。
下为对STL源码的框图分析:
1.4 关于源码设计的小吐槽
STL 源码的模板参数命名风格其实是并不完全统一的:
- set 的模板参数用
Key命名; - map 的模板参数用
Key和T(映射值类型)命名; - 红黑树的模板参数用
Key和Value命名。
这种命名差异可能会给初学者带来困惑,但核心逻辑其实是一致的 —— 通过泛型参数的灵活配置,实现数据结构的复用。
二、模拟实现的核心思路
基于 STL 的设计思想,我们将来模拟实现 map 和 set,核心步骤如下:
- 实现一个通用的红黑树类(RBTree),支持泛型参数配置。
- 在 map 和 set 中封装红黑树,通过自定义仿函数(KeyOfT)实现 key 的提取。
- 实现迭代器(iterator),支持中序遍历(保证 map 和 set 的有序性)。
- 解决 key 不可修改的问题(set 的 key 完全不可改,map 的 key 不可改但 value 可改)。
- 实现 map 的
operator[]运算符,支持便捷的 key-value 访问和插入。
2.1 关键调整与优化
为了简化实现并提高可读性,我们可以对 STL 源码的命名和结构做出如下调整:
- 红黑树的模板参数统一命名为
K(key 类型)、T(节点存储数据类型)、KeyOfT(key 提取仿函数)。 - 去掉 STL 中复杂的哨兵节点(header),用
nullptr表示迭代器的end()。 - 红黑树的节点结构直接明确定义,避免源码中嵌套的结构体依赖。
三、完整代码实现
3.1 红黑树的实现(RBTree.h)
红黑树是 map 和 set 的底层核心,需要实现节点结构、插入(含平衡调整)、旋转、迭代器等核心功能。由于在之前的博客中已经为大家介绍过了红黑树的实现,这里就不再过多赘述了,感兴趣的话可以参考一下之前的博客:C++进阶:(七)红黑树深度解析与 C++ 实现
#include <iostream>
#include <utility>
using namespace std;// 红黑树节点颜色枚举
enum Colour {RED, // 红色节点BLACK // 黑色节点
};// 红黑树节点结构
template <class T>
struct RBTreeNode {T _data; // 节点存储的数据(set中是key,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), _col(RED) // 新节点默认红色(减少平衡调整次数){}
};// 红黑树迭代器
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){}// 前置++:迭代器指向中序遍历的下一个节点Self& operator++() {if (_node->_right) {// 情况1:右子树不为空,下一个节点是右子树的最左节点Node* leftMost = _node->_right;while (leftMost->_left) {leftMost = leftMost->_left;}_node = leftMost;} else {// 情况2:右子树为空,向上找第一个“当前节点是父节点左孩子”的祖先Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right) {cur = parent;parent = cur->_parent;}_node = parent; // 找到祖先或nullptr(end())}return *this;}// 前置--:迭代器指向中序遍历的前一个节点Self& operator--() {if (_node == nullptr) {// 情况1:当前是end(),--后指向最右节点(中序最后一个节点)Node* rightMost = _root;while (rightMost && rightMost->_right) {rightMost = rightMost->_right;}_node = rightMost;} else if (_node->_left) {// 情况2:左子树不为空,前一个节点是左子树的最右节点Node* rightMost = _node->_left;while (rightMost->_right) {rightMost = rightMost->_right;}_node = rightMost;} else {// 情况3:左子树为空,向上找第一个“当前节点是父节点右孩子”的祖先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; // 常量迭代器// 构造函数RBTree() : _root(nullptr) {}// 析构函数~RBTree() {Destroy(_root);_root = nullptr;}// 迭代器 begin():指向中序遍历的第一个节点(最左节点)Iterator Begin() {Node* leftMost = _root;while (leftMost && leftMost->_left) {leftMost = leftMost->_left;}return Iterator(leftMost, _root);}// 常量迭代器 begin()ConstIterator Begin() const {Node* leftMost = _root;while (leftMost && leftMost->_left) {leftMost = leftMost->_left;}return ConstIterator(leftMost, _root);}// 迭代器 end():指向nullptr(中序遍历的末尾)Iterator End() {return Iterator(nullptr, _root);}// 常量迭代器 end()ConstIterator End() const {return ConstIterator(nullptr, _root);}// 插入元素:返回pair<迭代器, bool>,bool表示是否插入成功(避免重复key)pair<Iterator, bool> Insert(const T& data) {// 情况1:树为空,直接创建根节点(根节点为黑色)if (_root == nullptr) {_root = new Node(data);_root->_col = BLACK;return make_pair(Iterator(_root, _root), true);}KeyOfT kot; // key提取仿函数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 {// 存在重复key,插入失败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;// 平衡调整:父节点为红色时需要调整(红黑树不能有连续红色节点)while (parent && parent->_col == RED) {Node* grandfather = parent->_parent; // 祖父节点(一定存在,因为根节点是黑色)// 情况1:父节点是祖父节点的左孩子if (parent == grandfather->_left) {Node* uncle = grandfather->_right; // 叔叔节点// 子情况1.1:叔叔节点存在且为红色(变色调整)if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 向上继续调整(祖父节点变为红色,可能与曾祖父节点冲突)cur = grandfather;parent = cur->_parent;} else {// 子情况1.2:叔叔节点不存在或为黑色(旋转+变色调整)// 子情况1.2.1:当前节点是父节点的左孩子(右单旋)if (cur == parent->_left) {RotateR(grandfather); // 对祖父节点右旋parent->_col = BLACK; // 父节点变黑grandfather->_col = RED; // 祖父节点变红} else {// 子情况1.2.2:当前节点是父节点的右孩子(左旋+右旋)RotateL(parent); // 对父节点左旋RotateR(grandfather); // 对祖父节点右旋cur->_col = BLACK; // 当前节点变黑grandfather->_col = RED; // 祖父节点变红}break; // 调整完成,跳出循环}} else {// 情况2:父节点是祖父节点的右孩子(与情况1对称)Node* uncle = grandfather->_left; // 叔叔节点// 子情况2.1:叔叔节点存在且为红色(变色调整)if (uncle && uncle->_col == RED) {parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 向上继续调整cur = grandfather;parent = cur->_parent;} else {// 子情况2.2:叔叔节点不存在或为黑色(旋转+变色调整)// 子情况2.2.1:当前节点是父节点的右孩子(左单旋)if (cur == parent->_right) {RotateL(grandfather); // 对祖父节点左旋parent->_col = BLACK; // 父节点变黑grandfather->_col = RED; // 祖父节点变红} else {// 子情况2.2.2:当前节点是父节点的左孩子(右旋+左旋)RotateR(parent); // 对父节点右旋RotateL(grandfather); // 对祖父节点左旋cur->_col = BLACK; // 当前节点变黑grandfather->_col = RED; // 祖父节点变红}break; // 调整完成,跳出循环}}}// 确保根节点始终为黑色_root->_col = BLACK;return make_pair(Iterator(newNode, _root), true);}// 根据key查找元素:返回迭代器Iterator Find(const K& key) {Node* cur = _root;KeyOfT kot;while (cur) {if (kot(cur->_data) < key) {cur = cur->_right;} else if (kot(cur->_data) > key) {cur = cur->_left;} else {// 找到key,返回对应的迭代器return Iterator(cur, _root);}}// 未找到,返回end()return End();}private:// 左旋转:以parent为旋转中心void RotateL(Node* parent) {Node* subR = parent->_right; // 父节点的右孩子Node* subRL = subR->_left; // 右孩子的左子树// 步骤1:将subRL链接到parent的右孩子parent->_right = subRL;if (subRL) {subRL->_parent = parent;}// 步骤2:处理parent的父节点Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;// 步骤3:如果parent是根节点,更新根;否则链接到祖父节点if (parentParent == nullptr) {_root = subR;subR->_parent = nullptr;} else {if (parent == parentParent->_left) {parentParent->_left = subR;} else {parentParent->_right = subR;}subR->_parent = parentParent;}}// 右旋转:以parent为旋转中心void RotateR(Node* parent) {Node* subL = parent->_left; // 父节点的左孩子Node* subLR = subL->_right; // 左孩子的右子树// 步骤1:将subLR链接到parent的左孩子parent->_left = subLR;if (subLR) {subLR->_parent = parent;}// 步骤2:处理parent的父节点Node* parentParent = parent->_parent;subL->_right = parent;parent->_parent = subL;// 步骤3:如果parent是根节点,更新根;否则链接到祖父节点if (parentParent == nullptr) {_root = subL;subL->_parent = nullptr;} else {if (parent == parentParent->_left) {parentParent->_left = subL;} else {parentParent->_right = subL;}subL->_parent = parentParent;}}// 递归销毁红黑树void Destroy(Node* root) {if (root == nullptr) {return;}Destroy(root->_left);Destroy(root->_right);delete root;}private:Node* _root; // 红黑树根节点
};
3.2 set 的实现(Myset.h)
set 是 key 的集合,其特点是 key 唯一且有序,key 不可修改。通过封装红黑树,并重定义KeyOfT仿函数实现。代码如下:
#include "RBTree.h"
#include <iostream>
using namespace std;namespace bit {template <class K>class set {// 自定义KeyOfT仿函数:从节点数据(K类型)中提取keystruct 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;// 普通迭代器 begin()iterator begin() {return _t.Begin();}// 普通迭代器 end()iterator end() {return _t.End();}// 常量迭代器 begin()const_iterator begin() const {return _t.Begin();}// 常量迭代器 end()const_iterator end() const {return _t.End();}// 插入元素:返回pair<iterator, bool>,bool表示是否插入成功pair<iterator, bool> insert(const K& key) {return _t.Insert(key);}// 根据key查找元素iterator find(const K& key) {return _t.Find(key);}private:// 红黑树对象:第二个模板参数为const K,确保key不可修改RBTree<K, const K, SetKeyOfT> _t;};// 测试setvoid test_set() {bit::set<int> s;int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};for (auto e : a) {auto ret = s.insert(e);if (ret.second) {cout << "插入成功:" << e << endl;} else {cout << "插入失败(重复key):" << e << endl;}}// 遍历set(中序遍历,有序)cout << "set遍历(有序):";for (auto e : s) {cout << e << " ";}cout << endl;// 测试findauto it = s.find(5);if (it != s.end()) {cout << "找到key:" << *it << endl;} else {cout << "未找到key:5" << endl;}// 测试key不可修改(编译报错)// *it = 100; // 错误:set的key是const类型,不可修改}
}
3.3 map 的实现(Mymap.h)
map 是 key-value 的映射容器,其特点是 key 唯一且有序,key 不可修改但 value 可修改。同样通过封装红黑树实现,核心是KeyOfT仿函数和operator[]的实现。
#include "RBTree.h"
#include <iostream>
#include <string>
using namespace std;namespace bit {template <class K, class V>class map {// 自定义KeyOfT仿函数:从pair<const K, V>中提取key(first成员)struct MapKeyOfT {const K& operator()(const pair<const 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;// 普通迭代器 begin()iterator begin() {return _t.Begin();}// 普通迭代器 end()iterator end() {return _t.End();}// 常量迭代器 begin()const_iterator begin() const {return _t.Begin();}// 常量迭代器 end()const_iterator end() const {return _t.End();}// 插入元素:返回pair<iterator, bool>,bool表示是否插入成功pair<iterator, bool> insert(const pair<K, V>& kv) {return _t.Insert(kv);}// 根据key查找元素iterator find(const K& key) {return _t.Find(key);}// operator[]:支持map[key]访问和插入V& operator[](const K& key) {// 插入key-value对(value为默认构造),返回pair<iterator, bool>pair<iterator, bool> ret = insert(make_pair(key, V()));// 返回value的引用(可修改)return ret.first->second;}private:// 红黑树对象:节点存储pair<const K, V>,确保key不可修改RBTree<K, pair<const K, V>, MapKeyOfT> _t;};// 测试mapvoid test_map() {bit::map<string, string> dict;// 插入元素dict.insert({"sort", "排序"});dict.insert({"left", "左边"});dict.insert({"right", "右边"});// 使用operator[]修改valuedict["left"] = "左边(剩余)";// 使用operator[]插入新元素(key不存在时自动插入)dict["insert"] = "插入";dict["string"]; // value为默认构造(空字符串)// 遍历map(中序遍历,按key有序)cout << "map遍历(按key有序):" << endl;for (auto it = dict.begin(); it != dict.end(); ++it) {// 测试key不可修改(编译报错)// it->first += 'x'; // 错误:key是const类型// 修改valueit->second += "x";cout << it->first << " : " << it->second << endl;}// 测试findauto it = dict.find("insert");if (it != dict.end()) {cout << "找到key:" << it->first << ",value:" << it->second << endl;} else {cout << "未找到key:insert" << endl;}}
}
四、核心细节解析
4.1 红黑树的平衡规则与调整
红黑树的核心是通过以下规则维持平衡的,确保所有操作的时间复杂度为 O (log n):
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 所有叶子节点(NIL 节点)是黑色。
- 如果一个节点是红色,那么它的两个子节点都是黑色(无连续红色节点)。
- 从任意节点到其所有叶子节点的路径上,黑色节点的数量相同(黑高相同)。
当插入新节点(默认红色)时,可能会破坏规则 4,因此需要通过变色或旋转 + 变色进行调整。调整的核心思路是:
- 如果叔叔节点是红色:将父节点、叔叔节点变为黑色,祖父节点变为红色,向上继续调整。
- 如果叔叔节点是黑色或不存在:通过旋转(左旋、右旋、双旋)调整树的结构,再变色维持规则。
4.2 迭代器的实现原理
map 和 set 的迭代器需要支持中序遍历(保证有序性),其核心是operator++和operator--的实现:
4.2.1 operator++(中序下一个节点)
- 如果当前节点的右子树不为空:下一个节点是右子树的最左节点(中序遍历右子树的第一个节点)。
- 如果当前节点的右子树为空:向上查找第一个 “当前节点是父节点左孩子” 的祖先(该祖先即为下一个节点)。
4.2.2 operator--(中序前一个节点)
- 如果当前是
end()(nullptr):前一个节点是树的最右节点(中序遍历的最后一个节点)。 - 如果当前节点的左子树不为空:前一个节点是左子树的最右节点(中序遍历左子树的最后一个节点)。
- 如果当前节点的左子树为空:向上查找第一个 “当前节点是父节点右孩子” 的祖先(该祖先即为前一个节点)。
4.3 key 不可修改的实现
- set 的 key 不可修改:set 封装红黑树时,将红黑树的节点存储类型设为
const K,因此迭代器解引用后得到的是const K&,无法修改。 - map 的 key 不可修改:map 的节点存储类型是
pair<const K, V>,其中 key 被定义为const,因此迭代器访问it->first时是const K&,无法修改;而it->second是V&,可以修改。
4.4 map 的 operator [] 实现
map 的operator[]是最常用的接口,其实现逻辑如下:
V& operator[](const K& key) {pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;
}
- 调用
insert插入(key, V())(value 为默认构造)。 - 如果 key 已存在,
insert返回已存在节点的迭代器和false,直接返回该节点的 value 引用。 - 如果 key 不存在,
insert插入新节点并返回新节点的迭代器和true,返回新节点的 value 引用。
这种实现支持三种用法:
- 访问已存在的 key:
dict["left"]。 - 修改已存在的 key 的 value:
dict["left"] = "左边(剩余)"。 - 插入新的 key-value 对:
dict["insert"] = "插入"。
总结
通过本文的实现,希望大家可以深入理解 map 和 set 的底层机制,掌握红黑树的核心原理和泛型编程的思想,为后续学习更复杂的容器和数据结构打下坚实基础。
