【C++进阶】 用红黑树封装map和set

本文详细解析了C++STL中set和map如何共用同一红黑树模板,展示了底层结构、插入操作改造、迭代器实现以及[]操作符的处理,揭示了模板参数对存储结构的影响。
摘要由CSDN通过智能技术生成

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


一、set和map源码剖析

源码地址:点击跳转

我们首先可以观察到,在setmap中包含有如下的头文件

在这里插入图片描述

先剧透一下:我们知道,setkey模型,mapkey-value模型,按道理来说应该用两颗不同的红黑树分别来封装keykey-value。但其实在源码实现中,它们使用的是同一个模板

那么它是如何做到的呢?

我们首先可以打开stl_set.h头文件以及stl_map.h来剖析。

  • stl_set.h头文件

在以往的博客中讲过,看源码首先需要看它的成员变量

在这里插入图片描述

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

  • stl_map.h头文件

在这里插入图片描述

我们发现:mapkey_value结构,但它的value竟然是个pair结构

也就是说:

  • set<K,K>模型的红黑树
  • map<K,pair>模型的红黑树

为什么STL要这样设计呢?由于 setmap的底层是红黑树,接下来我们再看看红黑树stl_tree.h的实现

  • stl_tree.h头文件

在这里插入图片描述

以上源代码的核心是link_type header,其类型是rb_tree_node*,也就是红黑树结点的指针。

接下来我们需要关心这颗树的Value

在这里插入图片描述

大家发现没有 结点存储的是Value

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

我们现在再来观测一下这个红黑树结点里面有什么

在这里插入图片描述

我们发现:这里是通过一个继承关系来搞定的。派生类存储的是value,而基类存储的是三叉链的指针和颜色。

那么在这里我们似乎看到了,在这棵树里面,我们好像并不是很需要key-value结构中的key类型的模板参数,那么事实上是如此的吗?其实不是的,这个key还必须得传入,因为会有一些接口需要key。比如对于map而言,find需要通过key来查找。

并且还可以发现:mapset的接口本质都是调用红黑树的接口

在这里插入图片描述

二、改造红黑树

2.1 红黑树基本结构的改造

在前头说过:真正决定红黑树里面存储的是什么,是由第二个模板参数决定的

【RBTree.h】

在这里插入图片描述

同时也可以写出mapset的基本结构

【set.h】

在这里插入图片描述

【map.h】

在这里插入图片描述

2.2 插入操作的改造

首先我们来分析插入的模板参数应该是什么?对于set就是key;对于map则是pair。那么模板参数应该是V

那现在就会遇到一个非常棘手的问题:插入的位置代码不怎么好写。

在这里插入图片描述

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

在这里插入图片描述

解决方法:写一个仿函数首先在mapset里面写两个内部类,这个内部类里面是一个运算符重载,充当仿函数。对于map返回pairfirst,对于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那么如果右树不为空,访问的是右树的最左结点

在这里插入图片描述

当然了,--操作就是++反向操作,因为++走的是中序遍历(左子树,根,右子树),那么--就是右子树,根,左子树。

  • 左树不为空,则找左树的最右结点
  • 左树为空,则找孩子是父亲右的祖父结点

在这里插入图片描述

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

在这里插入图片描述

然后我们依次到mapset层次去封装

  • 【set.h】

在这里插入图片描述

  • map.h

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

  • 对于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.hset.hinsert返回值

在这里插入图片描述

在这里插入图片描述

【对RBTree.h修改】

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

在这里插入图片描述

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

在这里插入图片描述
在这里插入图片描述

这是因为:_set是一个普通对象,去红黑树调用返回的是普通的迭代器,而在set中,迭代器都是const迭代器,因此就会导致类型不匹配

我们可以参考库里面是怎么处理的

【stl_set.h】

在这里插入图片描述

我们可以尝试仿照它写

在这里插入图片描述

但是还是编译不通过

在这里插入图片描述
这时因为:

  1. 普通对象_set调用insert返回的是<iterator, bool>,这里迭代器是普通迭代器。
  2. 但是set中的迭代器为了防止key被修改,iteratorconst_iterator都封装的是红黑树const_iterator

因此想要从pair<iterator, bool>转换pair<const_iterator, bool>,会调用pair的构造函数:

在这里插入图片描述

正常情况下普通迭代器不能转化为const迭代器。

为了解决这种情况,我们在迭代器类内添加一个拷贝构造即可。

在这里插入图片描述

  • 当传进来的是普通迭代器的时候,Iterator是普通迭代器,这个函数相当于拷贝构造
  • 当传进来的是const迭代器的时候,Iterator依然是普通迭代器,此时该函数就相当于构造函数(普通迭代构造const迭代器)

那么最后这个[]操作就很容易实现了

在这里插入图片描述

五、本篇源代码以及stl相关源码

Gitte仓库:点击跳转

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值