C++STL系列之set和map系列


前言

set和map都是关联式容器,stl中树形结构的有四种,set,map,multiset,multimap.本次主要是讲他们的模拟实现和用法。


一、set、map、multiset、multimap

set

set的中文意思是集合,集合就说明不允许重复的元素

1...set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。

  1. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。

  2. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。

  3. set在底层是用红黑树来实现的
    6. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。这个后面模拟实现会讲原因。

  4. 使用set的迭代器遍历set中的元素,可以得到有序序列

  5. set中的元素默认按照小于来比较
    8.set中的元素不可以重复,所以可以用set来去重
    9.set中的迭代器是双向迭代器

T是key,Compare是仿函数来控制比较规则,Alloc是空间配置器

构造函数没什么可说的,看一眼就懂了,这里的T有很大意义,后面模拟会提到。

重要函数

画横线的就是比较重要的,有几个注意事项:

1.关于insert,库里面的insert是提供迭代器位置插入的,这样做的目的有两个:

1.与其他stl的insert保持一致,比如vector,list都提供迭代器插入

2.看下面的emplace_hint,emplace版本的构造一般是直接在容器内部构造,而emplace_hint对应的版本就是迭代器位置插入,hint是提示,若pos指向的位置恰好是元素应插入的位置或邻近位置(如插入有序序列时,pos指向当前最后一个元素),红黑树可直接从pos开始验证,跳过大部分查找步骤,插入效率接近 O (1);​即使pos是错误提示,set 仍会按正常逻辑查找正确位置(退化至 O (log n)),但不会破坏元素的有序性

2.count就是计数,set里一定是一,这个接口是给multi版本使用的

3.lower_bound是大于等于元素的位置,upper_bound是大于元素的位置,这样是为了维护迭代器左闭右开的性质

map

1... map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。

  1. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:

typedef pair<const key, T> value_type;

  1. 在内部,map中的元素总是按照键值key进行比较排序的。

  2. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。

  3. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。

  4. map通常被实现为红黑树

第二个参数给的是T而不是Value,这里也有很大意义,后面模拟实现会提到,其他和set一样。

重要函数

map的重要接口和set类似,只多了比较重要的[] 和 at,都是通过key返回value的引用,他们的实现都得益于insert

\]功能很强大,支持修改,插入、插入 + 修改 ```cpp int main() { map mp; mp.insert({ 0,1 }); mp[0] = 2; mp[2]; mp[3] = 3; return 0; } ``` \[\]的实现实际上是这样,模拟实现还需要使用 ```cpp V& operator[](const K& key) { pair pr = make_pair(key,V()); iterator it = insert(pr).first; return *it.second; } ``` ### multi系列 multi中文意思是多重,就意味着允许键值对冗余,也就是key可以重复。 count的作用是给他的,如果想使用可以重复的集合就用multiset multimap中没有提供\[\],因为key是可以重复的,那我该返回哪个呢?所以就不提供 ## 二、模拟实现 set和map的使用都比较简单,主要还是在模拟上。 ### 框架 第一个问题就来了,一个是key模型,一个是key-value模型,难道要写成两份代码吗?之前list的迭代器提到了,其实库里面很杜绝这种相似代码写两份的,我们看看库里是怎么解决的,其实上面提供了一点消息就是T ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/e8f28ad891414af297be19299bbe4440.png) 都搞成了两个参数\,其中T对于set就是K,对于map就是pair\< K,V\> ,这样就可以通过一份代码来实现set和map,先搭一个框架,节点存放的就是T类型了 ```cpp template struct RBTreeNode { RBTreeNode* _left; RBTreeNode* _right; RBTreeNode* _parent; Color color; T _kv; RBTreeNode(const T& p) :_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,color(Color::RED) ,_kv(p) {} }; template class RBTree { typedef RBTreeNode Node; private: Node* _root; } ``` set里面放的就是`RBTree _t;` map里面就是`RBTree _t;` 到了插入,又出现新问题了,现在insert能跑了吗?`bool insert(const T& p)`传的是T啊,对于set倒是能比大小,map呢?pair有一套自己的比较大小的规则,但是和实际插入的规则不同啊,所以还需要一个参数KofT,取出T中的K map: ```cpp struct mapKOfT { const K& operator()(const pair& kv) { return kv.first; } }; private: RBTree,mapKOfT> _t; ``` set: ```cpp struct SetKOfT { const K& operator()(const K& kv) { return kv; } }; RBTree _t; ``` insert就会变成`KOfT koft;`涉及到比大小都用koft套一层 到目前为止,基本的框架已经完事了。下一步,迭代器 ### 迭代器 先看库里的迭代器怎么玩的 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/54aaaff13197424b8f206623a1974d8f.png) set为什么要这么搞呢?因为set本身就不支持任何的修改,所以set这两个都是const_iterator 但是对于map,map的value要支持修改,key不支持,所以不能搞成const,那他的key怎么防止修改呢?这个后面提,其实第一张图片已经看到了。 主要还是++ -- begin end怎么实现: 迭代器架子很好搭建,因为有了list的基础 ### 框架 ```cpp template struct RBTree_Iterator { typedef RBTreeNode Node; typedef RBTree_Iterator Self; Node* _node; RBTree_Iterator(Node* node) :_node(node) {} Ref operator*() { return _node->_kv; } Ptr operator->() { return &_node->_kv; } Self& operator--(){} Self operator--(int){} Self operator++(int){} bool operator!=(const Self& ot)const { return _node != ot._node; } bool operator==(const Self& ot)const { return _node == ot._node; } }; ``` 红黑树里面: ```cpp typedef RBTree_Iterator iterator; typedef RBTree_Iterator const_iterator; ``` ## 关于begin end ++ 和 - - 迭代器区间是左闭右开,begin就是最小元素,end呢,可以给空吗?不可以,因为要支持--end()走到上一个元素,库里的实现是搞了个类似哨兵位头节点的东西。 **这个头节点,左孩子是最小节点也就是begin(),右孩子是最大节点, 头节点的父亲是根,根的父亲是头节点** ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a67460a25027425eb6c79e2161fb0543.png) 但是这样维护起来比较麻烦,模拟实现只是为了了解底层,造不出更好的。所以这里不使用头节点方式,end就按空节点来存放(但实际上不是这样) ### begin begin其实就是找到最左节点返回就可以 ```cpp iterator begin() { Node* cur = _root; while (cur && cur->_left) cur = cur->_left;//考虑cur主要是考虑这是一颗空树的问题 return iterator(cur); } ``` ### ++ 和 - - ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/a36b7591fb9b498db793204a2c11da8b.png) 上代码: ```cpp Self& operator--() { Node* nodeleft = _node->_left; if (nodeleft) { while (nodeleft->_right) nodeleft = nodeleft->_right; _node = nodeleft; return *this; } else { Node* cur = _node; Node* parent = cur->_parent; while (parent && cur == parent->_left) { cur = parent; parent = cur->_parent; } _node = parent; return *this; } } Self operator--(int) { Node* tmp(_node); --(*this); return RBTree_Iterator(tmp); } Self& operator++() { Node* noderight = _node->_right; if (noderight) { while (noderight->_left) noderight = noderight->_left; _node = noderight; } else { Node* cur = _node; Node* parent = cur->_parent; //父亲有可能会为空,比如三角形结点的树 while (parent && cur == parent->_right) { cur = parent; parent = cur->_parent; } //如果父亲是空,说明这棵树已经遍历完全了 //如果父亲不是空,正常父亲给Node _node = parent; } return *this; } Self operator++(int) { Self tmp(_node); ++(*this); return tmp; } ``` 此时基本上红黑树里面的迭代器基本完全了,实现map和set 注意:这里不加typename编译不通过,原因: 类模板没有被实例化,没有生成具体代码,编译器不敢去里面找iterator 都没有检查代码错误,取到的有可能是内部类或者静态成员变量或者类型,加上typename就是告诉他是类型 ```cpp //set: typedef typename RBTree::const_iterator iterator; typedef typename RBTree::const_iterator const_iterator; //map: typedef typename RBTree, mapKOfT>::iterator iterator; typedef typename RBTree, mapKOfT>::const_iterator const_iterator; ``` 对于set的begin、end 和 cbegin、cend接口,const版本正常写,这里如果再写一个非const版本的会报错误,因为既然是非const,this指针会调用红黑树里非const版本的begin,返回非const迭代器,此时无法传给set里的const迭代器,所以只需要实现一个。 ```cpp iterator begin()const { return _t.begin(); } iterator end()const { return _t.end(); } ``` 对于map的begin、end系列正常就可以了 ```cpp iterator begin() { return _t.begin(); } iterator end() { return _t.end(); } const_iterator begin()const { return _t.begin(); } const_iterator end()const { return _t.end(); } ``` ### set、map防止修改 刚才已经讲了,set防止修改的方式就是迭代器都是const迭代器 那map呢?map不想让key修改,可以让value修改,来看库里的实现 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/71425650d41645678dfdef8d3e24b456.png) 他把那个T里面的key都换成了const key,我们也这么实现。 有的人会有疑问?那我map\ mp;不是有两个const吗?不会报错吗? 不会,C++对重复的const编译器会进行优化成一个const。 ```cpp typedef typename RBTree, mapKOfT>::iterator iterator; typedef typename RBTree, mapKOfT>::const_iterator const_iterator; RBTree,mapKOfT> _t; ``` 这样map就做到了不能修改K但是可以修改V的效果。 ### map的【】实现 上面说了,第一件事就是改insert的返回值,主要就是改return的地方,搞成一个pair就可以了 ```cpp pair insert(const T& p) { KOfT koft; if (_root == nullptr) { _root = new Node(p); _root->color = Color::BLACK; return make_pair(iterator(_root),true); } else { Node* cur = _root; Node* parent = nullptr; while (cur) { //用T了之后还有点问题 set是K,map是pair怎么比? //再传一个模板用来取出key即可 if (koft(cur->_kv) < koft(p)) { parent = cur; cur = cur->_right; } else if (koft(cur->_kv) > koft(p)) { parent = cur; cur = cur->_left; } else { return make_pair(iterator(cur), false); } } cur = new Node(p); Node* newnode = cur; if (koft(parent->_kv) < koft(p)) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent; //这之前都是搜索树的逻辑 // while (parent && parent->color == Color::RED) { Node* grandf = parent->_parent; if (grandf->_left == parent) { Node* uncle = grandf->_right; //叔叔存在且为红 if (uncle && uncle->color == Color::RED) { //判断 uncle->color = parent->color = Color::BLACK; grandf->color = Color::RED; //继续判断 cur = grandf; parent = cur->_parent; } //叔叔不存在或者为黑 else { //开旋 // if (parent->_left == cur) { RotateRight(grandf); parent->color = Color::BLACK; grandf->color = Color::RED; } else { //parent->right 是cur,grandf的左边是parent RotateLeft(parent); RotateRight(grandf); cur->color = Color::BLACK; grandf->color = Color::RED; } } } //祖父的右边是父亲 else { Node* uncle = grandf->_left; //uncle不存在 或者uncle为黑 //uncle为红 if (uncle && uncle->color == Color::RED) { parent->color = uncle->color = Color::BLACK; grandf->color = Color::RED; cur = grandf; parent = cur->_parent; } //uncle为黑或者不存在 else { //旋转 // grandf //uncle parent //cur if (cur == parent->_right) { RotateLeft(grandf); parent->color = Color::BLACK; grandf->color = Color::RED; } else { RotateRight(parent); RotateLeft(grandf); cur->color = Color::BLACK; grandf->color = Color::RED; } } } } _root->color = Color::BLACK; return make_pair(iterator(newnode), true); } } ``` 改完之后就可以实现operator\[\]了,一句话看着不舒服可以分开 ```cpp V& operator[](const K& key) { return (*(insert(make_pair(key, V())).first)).second; } ``` set的insert也需要跟着改。 改完之后跑了一段代码,发现set的跑不过,map的可以,为什么呢? ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/52386d39871847dd927ab8f023f4fd9b.png) 所以在迭代器里需要提供一个普通迭代器构造const迭代器,库里面实现的很牛。 ```cpp typedef RBTree_Iterator iterator; RBTree_Iterator(const iterator& it)//可以拿普通迭代器构造const迭代器 :_node(it._node) {} ``` 搞了一个迭代器参数永远是\无论外面传进来是const迭代器还是普通迭代器这里都是普通迭代器。 对于下面这个构造函数: 1.如果是普通迭代器,相当于普通迭代器的拷贝构造 2.如果是const迭代器,相当于普通迭代器来构造const迭代器。这样就能跑了。 另外,这一块其实也是库里一直都有的设计。任何的容器都支持const迭代器给非const迭代器 ```cpp list lt; list::const_iterator it = lt.begin(); ``` *** ** * ** *** ## 三、完整代码 [个人gitee](https://gitee.com/taurus123jl/cpp--stl/tree/master/map%E5%92%8Cset/map%E5%92%8Cset) ## 总结 map和set的东西不少,需要好好练习和掌握。

相关推荐
洁可2 小时前
上位机程序开发基础介绍
c++·笔记
寒心雨梦3 小时前
本地preload hook案例
c++
滴水成川4 小时前
现代 C++ 开发工作流(VSCode / Cursor)
开发语言·c++·vscode·cursor
张同学的IT技术日记4 小时前
重构 MVC:让经典架构完美适配复杂智能系统的后端业务逻辑层(内附框架示例代码)
c++·后端·重构·架构·mvc·软件开发·工程应用
万能的小裴同学4 小时前
星痕共鸣数据分析2
c++·数据分析
刚入坑的新人编程5 小时前
暑期算法训练.8
数据结构·c++·算法·面试·哈希算法
TalkU浩克5 小时前
C++中使用Essentia实现STFT/ISTFT
开发语言·c++·音频·istft·stft·essentia
小比卡丘6 小时前
【C++进阶】第7课—红黑树
java·开发语言·c++
序属秋秋秋8 小时前
《C++初阶之STL》【vector容器:详解 + 实现】
开发语言·c++·笔记·学习·stl
lixzest12 小时前
快速梳理遗留项目
java·c++·python