
👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
一、set和map源码剖析
源码地址:点击跳转
我们首先可以观察到,在set和map中包含有如下的头文件

先剧透一下:我们知道,set是key模型,map是key-value模型,按道理来说应该用两颗不同的红黑树分别来封装key和key-value。但其实在源码实现中,它们使用的是同一个模板。
那么它是如何做到的呢?
我们首先可以打开stl_set.h头文件以及stl_map.h来剖析。
stl_set.h头文件
在以往的博客中讲过,看源码首先需要看它的成员变量

我们发现:set的底层好像是key-value结构(和我们一开始说的key模型不太一样),不同的是它们都是Key。接下来我们再来看看stl_map.h头文件
stl_map.h头文件

我们发现:map是key_value结构,但它的value竟然是个pair结构
也就是说:
set是<K,K>模型的红黑树map是<K,pair>模型的红黑树
为什么STL要这样设计呢?由于 set和map的底层是红黑树,接下来我们再看看红黑树stl_tree.h的实现
stl_tree.h头文件

以上源代码的核心是link_type header,其类型是rb_tree_node*,也就是红黑树结点的指针。
接下来我们需要关心这颗树的Value。

大家发现没有 结点存储的是Value。
但是这个Value不是我们前面所说的key-val模型中的val。这里的Value对于map而言是pair,对于set而言是key。所以说,真正决定红黑树里面存储的是什么,是由第二个模板参数决定的,这也就为什么map和set可以共用一颗树。其实也不是,更严谨一点来说是共用同一个类模板(红黑树)。
我们现在再来观测一下这个红黑树结点里面有什么

我们发现:这里是通过一个继承关系来搞定的。派生类存储的是value,而基类存储的是三叉链的指针和颜色。
那么在这里我们似乎看到了,在这棵树里面,我们好像并不是很需要key-value结构中的key类型的模板参数,那么事实上是如此的吗?其实不是的,这个key还必须得传入,因为会有一些接口需要key。比如对于map而言,find需要通过key来查找。
并且还可以发现:map和set的接口本质都是调用红黑树的接口

二、改造红黑树
2.1 红黑树基本结构的改造
在前头说过:真正决定红黑树里面存储的是什么,是由第二个模板参数决定的。
【RBTree.h】

同时也可以写出map和set的基本结构
【set.h】

【map.h】

2.2 插入操作的改造
首先我们来分析插入的模板参数应该是什么?对于set就是key;对于map则是pair。那么模板参数应该是V。
那现在就会遇到一个非常棘手的问题:插入的位置代码不怎么好写。

原因是:假设是set,V在实例化后,data的类型此时是没有问题的;如果是map,V在实例化后,data是pair,此处应该比较pair中的first,而pair的重载和我们期望的不一样,它默认先比first,再比second

解决方法:写一个仿函数。首先在map和set里面写两个内部类,这个内部类里面是一个运算符重载,充当仿函数。对于map返回pair的first,对于set返回它本身,也就是key
- 【set.h】

-
【map.h】

-
【RBTree.h】

2.3 添加查找操作
【RBTree.h】

【map.h】
【set.h】
三、迭代器
首先来看看库里的set的迭代器是如何实现的

如上图所示,迭代器同样是依靠着红黑树里面的迭代器去实现的。那么我们可以去stl_tree.h文件看看

我们可以看到,这个迭代器与list的迭代器是比较相似的。

接下来可以简单看看迭代器里的操作

大家注意到没,这里的++和--操作和我们在模拟实现list的时候一样,封装了一个结点的指针,然后重载运算符。但是这是一颗红黑树啊,++和--往哪走?

首先可以想到:迭代器遍历的时候默认是升序,也就是中序遍历。那么 a.begin()返回的一定是树中的最小值。比如访问结点7。所以,如果右树为空,那么下一个访问的就是孩子是父亲的左孩子的祖先;假设记下来访问10,那么如果右树不为空,访问的是右树的最左结点。

当然了,--操作就是++反向操作,因为++走的是中序遍历(左子树,根,右子树),那么--就是右子树,根,左子树。
- 左树不为空,则找左树的最右结点
- 左树为空,则找孩子是父亲右的祖父结点

有了迭代器类,我们可以在红黑树层次去调用这个迭代器类了

然后我们依次到map和set层次去封装
- 【set.h】

- 【
map.h】

但是以上代码还是不完美,不管是set和map,都可以修改容器里的数据,因为一旦修改了树里面的数据,那么就意味着这棵树已经乱了。可能不再是一个搜索二叉树了。

而在库里面是这样实现的:

- 对于
set,普通迭代器和const迭代器本质都是const迭代器。 - 对于
map,它的key应该不可以被修改,而value是可以被修改的(参考统计次数),这里是通过对pair的第一个参数first进行const限定的,从而锁死了第一个参数。
【RBTree.h】

当然了,咱们自定义类型的迭代器也得修改

然后我们来修改set中的迭代器函数。

【测试】

当测试时,就连普通迭代器都运行不起来了。我们可以参考库中是如何实现的

【修改set.h后】

然后发现可以打印出结果,这是为什么呢?
如果不在后面加const,那么_set就是一个普通对象,那么begin()返回的就是普通begin(),而普通的iterator已经被重命名为const_iterator,因此就会导致返回类型不匹配。
或者直接提供const版本的begin()和end()也可以,因为set的普通对象和const对象都是调用const迭代器(权限可以缩小和平移)

然后我们来处理map的迭代器问题
处理方式很简单:只需要让pair中的第一个参数给带上const即可。保证key不可以被修改,而value可以修改。
【map.h】

四、[] 操作符

map的[]操作主要是依靠insert操作实现的。所以还得将insert在进一步完善
首先分别修改map.h和set.h中insert返回值


【对RBTree.h修改】
除了修改函数的返回值,还需要返回指向新插入节点的那个迭代器

但是当我们运行的时候,报错了,我们发现是set中的pair的迭代器出问题了


这是因为:_set是一个普通对象,去红黑树调用返回的是普通的迭代器,而在set中,迭代器都是const迭代器,因此就会导致类型不匹配
我们可以参考库里面是怎么处理的
【stl_set.h】

我们可以尝试仿照它写

但是还是编译不通过

这时因为:
- 普通对象
_set调用insert返回的是<iterator, bool>,这里迭代器是普通迭代器。 - 但是
set中的迭代器为了防止key被修改,iterator和const_iterator都封装的是红黑树const_iterator。
因此想要从pair<iterator, bool>转换pair<const_iterator, bool>,会调用pair的构造函数:

正常情况下普通迭代器不能转化为const迭代器。
为了解决这种情况,我们在迭代器类内添加一个拷贝构造即可。

- 当传进来的是普通迭代器的时候,
Iterator是普通迭代器,这个函数相当于拷贝构造 - 当传进来的是
const迭代器的时候,Iterator依然是普通迭代器,此时该函数就相当于构造函数(普通迭代构造const迭代器)
那么最后这个[]操作就很容易实现了

五、本篇源代码以及stl相关源码
Gitte仓库:点击跳转
本文详细解析了C++STL中set和map如何共用同一红黑树模板,展示了底层结构、插入操作改造、迭代器实现以及[]操作符的处理,揭示了模板参数对存储结构的影响。
3231

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



