【C++游记】Map与Set的封装
枫の个人主页
你不能改变过去,但你可以改变未来
算法/C++/数据结构/C
Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记】
话不多说,让我们一起进入今天的学习吧~~~
目录
封装红黑树实现 mymap 和 myset 超详细教程
一、SGI-STL3.0 源码及框架分析
1.1 核心结构框架
set 的结构框架
map 的结构框架
红黑树的结构框架
1.2 关键设计思想解析
二、模拟实现 map 和 set
2.1 红黑树的实现
2.1.1 红黑树节点定义
2.1.2 红黑树迭代器实现
2.1.3 红黑树核心类实现
2.2 封装实现 set
2.3 封装实现 map
2.4 测试代码及结果
三、总结
四、结语
封装红黑树实现 mymap 和 myset 超详细教程
在 STL 容器中,map 和 set 是常用的关联式容器,它们的底层实现都依赖于红黑树这一高效的数据结构。本文将从 SGI-STL3.0 版本源码框架分析入手,详细讲解如何基于红黑树封装实现自定义的 mymap 和 myset,并深入剖析其中的关键技术点,包括红黑树的实现、仿函数的应用、迭代器设计以及 map 的 [] 运算符重载等内容。
一、SGI-STL3.0 源码及框架分析
要实现自定义的 mymap 和 myset,首先需要深入理解 STL 中 map 和 set 的底层实现框架。在 SGI-STL3.0 版本中,map 和 set 的源代码分别位于 map/set/stl_map.h、stl_set.h、stl_tree.h 等头文件中。
1.1 核心结构框架
set 的结构框架
#ifndef __SGI_STL_INTERNAL_TREE_H
#include <stl_tree.h>
#endif
#include <stl_set.h>
#include <stl_multiset.h>template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:// 类型定义typedef Key key_type;typedef Key value_type;
private:// 红黑树类型定义,作为set的底层存储typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // 用于表示set的红黑树
};
map 的结构框架
#ifndef __SGI_STL_INTERNAL_TREE_H
#include <stl_tree.h>
#endif
#include <stl_map.h>
#include <stl_multimap.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:// 红黑树类型定义,作为map的底层存储typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;rep_type t; // 用于表示map的红黑树
};
红黑树的结构框架
// 红黑树节点基类
struct __rb_tree_node_base {typedef __rb_tree_color_type color_type;typedef __rb_tree_node_base* base_ptr;color_type color; // 节点颜色base_ptr parent; // 父节点指针base_ptr left; // 左子节点指针base_ptr right; // 右子节点指针
};// 红黑树节点类
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base {typedef __rb_tree_node<Value>* link_type;Value value_field; // 节点存储的数据
};// 红黑树类
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree {
protected:typedef void* void_pointer;typedef __rb_tree_node_base* base_ptr;typedef __rb_tree_node<Value> rb_tree_node;typedef rb_tree_node* link_type;typedef Key key_type;typedef Value value_type;public:// 插入唯一元素(不允许重复)pair<iterator, bool> insert_unique(const value_type& x);// 删除指定key的元素size_type erase(const key_type& x);// 查找指定key的元素iterator find(const key_type& x);protected:size_type node_count; // 记录树的节点数量link_type header; // 头节点
};
1.2 关键设计思想解析
-
泛型设计:红黑树采用泛型思想实现,通过模板参数灵活控制其支持的场景。红黑树是实现 key 的搜索场景(set),还是 key/value 的搜索场景(map),由第二个模板参数
Value
决定__rb_tree_node
中存储的数据类型。- set 实例化红黑树时,第二个模板参数传入
key
,即红黑树节点存储的是 key 值。 - map 实例化红黑树时,第二个模板参数传入
pair<const Key, T>
,即红黑树节点存储的是键值对。
- set 实例化红黑树时,第二个模板参数传入
-
模板参数的作用:
- 第一个模板参数
Key
:主要用于find
、erase
等函数的参数类型,因为无论是 set 还是 map,查找和删除操作都是基于 key 进行的。对于 set 而言,Key
和Value
模板参数是相同的;但对于 map,Key
是键的类型,Value
是键值对的类型,二者完全不同。 - 第三个模板参数
KeyOfValue
:用于从Value
类型中提取出Key
,以便进行比较操作。例如在 map 中,需要从pair<const Key, T>
中提取出Key
来进行节点间的比较。
- 第一个模板参数
-
命名风格问题:需要注意的是,源码中的命名风格存在一定的不规范性。例如 set 的模板参数用
Key
命名,map 用Key
和T
命名,而红黑树又用Key
和Value
命名,这在一定程度上可能会造成理解上的混淆,在实际开发中需要尽量避免这种命名不一致的情况。
二、模拟实现 map 和 set
在理解了 STL 源码框架的基础上,我们开始模拟实现自定义的 mymap 和 myset。实现的核心思路是复用红黑树,并通过仿函数解决数据比较时的 key 提取问题,同时实现迭代器以支持遍历操作,最后为 map 实现 [] 运算符重载。
2.1 红黑树的实现
首先,我们需要实现一个通用的红黑树类,该类需要支持插入操作,并能通过仿函数从存储的数据中提取 key 进行比较。
2.1.1 红黑树节点定义
// 定义节点颜色枚举
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.1.2 红黑树迭代器实现
红黑树的迭代器实现是一个难点,其核心是实现operator++
和operator--
操作,以支持中序遍历(红黑树的中序遍历结果是有序的,这符合 map 和 set 的有序性要求)。
// 红黑树迭代器模板类
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) {// 右子树不为空,下一个节点是右子树的最左节点(中序遍历的下一个节点)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()的情况,end()指向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;}
};
2.1.3 红黑树核心类实现
红黑树的核心操作包括插入、旋转(左旋和右旋)、销毁等。插入操作后需要检查红黑树的性质是否被破坏,并进行相应的调整。
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() = default;// 析构函数~RBTree() {Destroy(_root);_root = nullptr;}// 获取begin迭代器(指向中序遍历的第一个节点,即最左节点)Iterator Begin() {Node* leftMost = _root;while (leftMost && leftMost->_left) {leftMost = leftMost->_left;}return Iterator(leftMost, _root);}// 获取end迭代器(指向nullptr,表示遍历结束)Iterator End() {return Iterator(nullptr, _root);}// 获取const版本的begin迭代器ConstIterator Begin() const {Node* leftMost = _root;while (leftMost && leftMost->_left) {leftMost = leftMost->_left;}return ConstIterator(leftMost, _root);}// 获取const版本的end迭代器ConstIterator End() const {return ConstIterator(nullptr, _root);}// 插入元素,返回pair<Iterator, bool>,其中bool表示插入是否成功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);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; // 祖父节点if (parent == grandfather->_left) {// 父节点是祖父节点的左孩子Node* uncle = grandfather->_right; // 叔叔节点if (uncle && uncle->_col == RED) {// 情况1:叔叔节点存在且为红色,只需变色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;} else {// 情况2:叔叔节点不存在或为黑色,需要旋转+变色if (cur == parent->_right) {// 子节点是父节点的右孩子,先左旋父节点RotateL(parent);swap(parent, cur); // 旋转后父节点和子节点的位置交换,需要更新parent和cur}// 右旋祖父节点RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;break; // 调整完成,退出循环}} else {// 父节点是祖父节点的右孩子Node* uncle = grandfather->_left; // 叔叔节点if (uncle && uncle->_col == RED) {// 情况1:叔叔节点存在且为红色,只需变色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;} else {// 情况2:叔叔节点不存在或为黑色,需要旋转+变色if (cur == parent->_left) {// 子节点是父节点的左孩子,先右旋父节点RotateR(parent);swap(parent, cur); // 旋转后父节点和子节点的位置交换,需要更新parent和cur}// 左旋祖父节点RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;break; // 调整完成,退出循环}}}// 确保根节点始终为黑色_root->_col = BLACK;return make_pair(Iterator(cur, _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 {// 找到元素,返回对应的迭代器return Iterator(cur, _root);}}// 未找到元素,返回end迭代器return End();}private:// 左旋操作void RotateL(Node* parent) {Node* subR = parent->_right; // 父节点的右子节点Node* subRL = subR->_left; // 右子节点的左子节点// 更新父节点的右子节点为subRLparent->_right = subRL;if (subRL) {subRL->_parent = parent;}Node* parentParent = parent->_parent; // 父节点的父节点// 更新subR的左子节点为parentsubR->_left = parent;parent->_parent = subR;// 更新parentParent的子节点为subRif (parentParent == nullptr) {// 父节点是根节点,旋转后subR成为新的根节点_root = subR;subR->_parent = nullptr;} else {if (parent == parentParent->_left) {parentParent->_left = subR;} else {parentParent->_right = subR;}subR->_parent = parentParent;}}// 右旋操作void RotateR(Node* parent) {Node* subL = parent->_left; // 父节点的左子节点Node* subLR = subL->_right; // 左子节点的右子节点// 更新父节点的左子节点为subLRparent->_left = subLR;if (subLR) {subLR->_parent = parent;}Node* parentParent = parent->_parent; // 父节点的父节点// 更新subL的右子节点为parentsubL->_right = parent;parent->_parent = subL;// 更新parentParent的子节点为subLif (parentParent == nullptr) {// 父节点是根节点,旋转后subL成为新的根节点_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 = nullptr; // 红黑树的根节点
};
2.2 封装实现 set
set 的特点是存储唯一的 key 值,且不允许修改 key。基于红黑树封装 set 时,需要定义一个仿函数SetKeyOfT
,用于从存储的 key 中提取 key(因为 set 存储的就是 key 本身,所以仿函数直接返回 key 即可)。
#include "RBTree.h"namespace wc {
template <class K>
class set {// 仿函数:从set存储的数据(K类型)中提取keystruct SetKeyOfT {const K& operator()(const K& key) {return key;}};public:// 迭代器类型定义,set的迭代器不允许修改数据,所以红黑树的第二个模板参数为const Ktypedef 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();}// 获取const版本的begin迭代器const_iterator begin() const {return _t.Begin();}// 获取const版本的end迭代器const_iterator end() const {return _t.End();}// 插入元素,返回pair<iterator, bool>pair<iterator, bool> insert(const K& key) {return _t.Insert(key);}// 查找指定key的元素iterator find(const K& key) {return _t.Find(key);}private:// 底层的红黑树对象RBTree<K, const K, SetKeyOfT> _t;
};// 测试set的函数
void test_set() {set<int> s;int a[] = {4, 2, 6, 1, 3, 5, 15, 7, 16, 14};for (auto e : a) {s.insert(e);}// 遍历set(中序遍历,结果有序)cout << "遍历set(正向):";for (auto e : s) {cout << e << " ";}cout << endl;// 反向遍历setcout << "遍历set(反向):";set<int>::const_iterator it = s.end();while (it != s.begin()) {--it;// *it += 2; // 错误,set的迭代器不允许修改数据cout << *it << " ";}cout << endl;// 查找元素auto findIt = s.find(6);if (findIt != s.end()) {cout << "找到元素:" << *findIt << endl;} else {cout << "未找到元素:6" << endl;}findIt = s.find(10);if (findIt != s.end()) {cout << "找到元素:" << *findIt << endl;} else {cout << "未找到元素:10" << endl;}
}
}
2.3 封装实现 map
map 的特点是存储键值对pair<const K, V>
,其中 key 不允许修改,value 允许修改。基于红黑树封装 map 时,需要定义一个仿函数MapKeyOfT
,用于从键值对中提取 key。同时,需要实现[]
运算符重载,以支持通过 key 快速访问和修改 value。
#include "RBTree.h"namespace wc {
template <class K, class V>
class map {// 仿函数:从map存储的数据(pair<const K, V>类型)中提取keystruct 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();}// 获取const版本的begin迭代器const_iterator begin() const {return _t.Begin();}// 获取const版本的end迭代器const_iterator end() const {return _t.End();}// 插入键值对,返回pair<iterator, bool>pair<iterator, bool> insert(const pair<const K, V>& kv) {return _t.Insert(kv);}// 查找指定key的元素iterator find(const K& key) {return _t.Find(key);}// []运算符重载,支持通过key访问和修改valueV& operator[](const K& key) {// 插入键值对,如果key已存在则返回已存在的迭代器,否则插入新的键值对(value为默认构造)pair<iterator, bool> ret = insert(make_pair(key, V()));// 返回对应的value引用return ret.first->second;}private:// 底层的红黑树对象RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};// 测试map的函数
void test_map() {map<string, string> dict;// 插入键值对dict.insert({"sort", "排序"});dict.insert({"left", "左边"});dict.insert({"right", "右边"});// 使用[]修改已存在的valuedict["left"] = "左边,剩余";// 使用[]插入新的键值对(value为默认构造的空字符串,之后再赋值)dict["insert"] = "插入";// 使用[]访问不存在的key,会插入一个键值对(value为默认构造的空字符串)dict["string"];// 遍历mapcout << "遍历map:" << endl;map<string, string>::iterator it = dict.begin();while (it != dict.end()) {// it->first += 'x'; // 错误,map的key不允许修改it->second += "x"; // 正确,map的value允许修改cout << it->first << ":" << it->second << endl;++it;}cout << endl;// 查找元素auto findIt = dict.find("sort");if (findIt != dict.end()) {cout << "找到元素:" << findIt->first << ":" << findIt->second << endl;} else {cout << "未找到元素:sort" << endl;}findIt = dict.find("erase");if (findIt != dict.end()) {cout << "找到元素:" << findIt->first << ":" << findIt->second << endl;} else {cout << "未找到元素:erase" << endl;}
}
}
2.4 测试代码及结果
在 main 函数中调用test_set
和test_map
函数,即可对自定义的 set 和 map 进行测试。
#include <iostream>
#include <string>
#include "Myset.h"
#include "Mymap.h"using namespace std;int main() {wc::test_set();cout << "-------------------------" << endl;wc::test_map();return 0;
}
三、总结
- 红黑树的复用:通过泛型设计,红黑树能够同时支持 set(存储 key)和 map(存储键值对)的底层实现,大大提高了代码的复用性。
- 仿函数的应用:仿函数
KeyOfT
解决了红黑树从存储的数据中提取 key 进行比较的问题,使得红黑树能够灵活处理不同类型的数据(key 或键值对)。 - 迭代器的设计:红黑树迭代器的核心是
operator++
和operator--
的实现,通过中序遍历的规则找到下一个或前一个节点,确保遍历结果的有序性。同时,set 的迭代器通过将红黑树的存储数据类型设为const K
,实现了 key 的不可修改;map 的迭代器通过将键值对的 key 设为const K
,实现了 key 的不可修改,而 value 可以修改。 - map 的 [] 运算符重载:基于红黑树的
Insert
操作,[]
运算符能够实现 key 的查找、插入(如果 key 不存在)以及 value 的访问和修改,大大简化了 map 的使用。
四、结语
今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~