【C++】 使用红黑树模拟实现STL中的map与set

这篇具有很好参考价值的文章主要介绍了【C++】 使用红黑树模拟实现STL中的map与set。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

前面的文章我们学习了红黑树,也提到了C++STL中的map和set的底层其实就是用的红黑树来实现的(而map和set的使用我们前面也学过了)。
既然红黑树我们也学习过了,那这篇文章我们就用红黑树来简单实现一下STL中的map和set,重点是学习它的框架。

1. 对之前实现的红黑树进行一些补充和完善

上一篇文章我们实现了红黑树,但是我们实现的那个类并不是特别完善,所以,为了后面map和set的模拟实现,我们先对之前写的那个红黑树的类做一些补充和完善。

1.1 析构

构造函数的话其实不写也没事,我们给了缺省值,但是析构的话有必要写一下,因为我们的红黑树是涉及资源管理的。

我们来实现一下:

二叉树的销毁我们之前也有讲过,走一个后序遍历销毁就行了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
代码很简单,我就不解释了

1.2 查找

那红黑树的查找呢我们上一篇文章有提到,但是当时没写,因为就跟搜索二叉树的查找一样嘛。

那这里由于后面我们要用红黑树模拟实现map和set(它们是有find这个接口的)的缘故,所以我们也补充一下:

直接上代码
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

2. STL源码中map和set的实现

那在正式实现之前,我们先一起来看一下STL(SGI版本)中map和set的源码,大致了解一下库里面是怎么实现的。

我们先来看一下map的源码:

由于源码里面内容比较多,我们这里不需要全部都看,所以我这里有选择性地提取一些内容给大家看
先来看这些
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
我们看到它的底层这个成员变量其实就是一棵红黑树,当然他这里面红黑树的类在另一个头文件里面实现,后面也会带大家看它里面的一些内容
然后我们继续:
我们之前说过map其实就对应搜索树的KV模型,那其实这里的key_typevalue_type就对应的是Key和Value的的类型,我们看到这里面的key_type就是key,但是value_type却是一个pair。
可以按我们之前的理解,map里面存的就是一个pair(键值对)嘛,pair的first对应key,second对应value。
但是它里面为什么value的类型就是一个pair呢?
那先不急,我们后面会解释。

然后我们再来看一下set:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
首先set的底层也是红黑树,这没什么说到,我们都知道,但是我们会发现map和set底层用的是同一个红黑树的类模板。
但是它们不是一个对应KV模型,一个对应K模型嘛,那我们想的可能是应该实现两个红黑树,一棵存Key的单个值,另一棵存KV的键值对,然后用这两棵红黑树分别封装map和set。

那它这里是如何做到map和set底层都用同一个红黑树的类模板呢?

观察set的结构我们会发现:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
set的里面虽然存的是单个值,可它除了有key_type之外,也有value_type,但是set里面的key_type和value_type都是Key的typedef,所以相当于传来两个key。
当然从表面上来看他之所以这样做的原因是他和map用的是同一个红黑树的类模板,而这个类模板是有两个参数的,所以它必须传两个。

那红黑树为什么要这样设计呢?所以我们再来观察一下红黑树的实现:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这里这个node_count其实就是记录了结点的数量,这个map和set需要获取size的时候就很方便,不需要再去遍历计数了。
然后下面这个link_type这个类型其实就是结点的指针,这个header代表啥我们可以先不管。
然后我们看到红黑树这里的模板参数是固定的,前两个模板参数是key和value,而红黑树结点里面放的其实就是value
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

所以它们这里这两个模板参数的传递是这样的:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这样如果是set,那红黑树的结点里面存的就是key,如果是map,那红黑树的结点里面存的就是pair的键值对。
所以这里第二个模板参数value接收的什么,就决定了红黑树结点里面存的是什么类型的数据。

那问题来了,第一个模板参数的作用是啥?

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
大家想一下map和set都有find,erase这些接口,那它们有什么不同?
🆗,对于set来说,它的查找是不是就是按结点里面存的那个key去查找的啊,返回的是对应元素的迭代器。
而map呢?
它里面存的是KV的键值对,但是它查找的时候是不是只拿K去查找啊
,因为key是唯一的,而value其实是可以重复的。
而map的查找返回的是整个pair的迭代器(其实还是结点里面元素的迭代器,map里面存的就是pair嘛。)

所以,我想大家现在应该就明白第一个模板参数Key是干嘛的了:

因为map里面存的是KV的键值对,但是查找这些操作的时候是以K为目标去查找的,所以红黑树里面要多搞出来一个参数Key来单独获取这个Key。
那其实对于set来说是不需要的(因为set里面存的就是单独一个key,查找也是用的key,上面我们也看了,它传的前两个模板就是一样的,都是key),但是因为你set要和map共用一个红黑树模板,所以你必须迎合这个红黑树的结构,也去多传一个。
当然其实map也可以不用第一个参数,查找的时候用pair.first就行了,但是set里面只存了一个key,它可找不来什么first的东西,所以红黑树才要增加一个模板参数,这样红黑树里面的find,erase这些函数就可以以一种统一的实现(即都用第一个模板参数接收的key去查找),这样map和set才可以共同复用同一个红黑树的模板类。

当然它们只是共用了同一个类模板而已,最后实例化出来的还是不同的类对象,但是这不就正是模板出现的主要意义嘛,实现代码的复用,对我们程序员来说还是方便了很多的。

那源码呢我们就先看到这里,接下来我们就来自己尝试模拟实现一下,其实就是去对红黑树进行一个封装嘛。
后面有需要我们再来看相应的源码。

3. 改造红黑树+封装map和set

3.1 红黑树结构修改

先要封装的话首先我们得对我们之前自己实现得红黑树做一些改造。

首先结点的结构我们可以不用改,虽然我们的跟库里面源码不太一样:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
我们这里就还用我们自己写的就可以,只是实现方式不同,这里没什么影响。

那红黑树的结构我们就需要修改一下了:

因为我们当时是按照K模型实现的,只有一个模板参数【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
所以要加一个,至于这里为什么需要两个上面已经解释过了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这里我们就用KT,大家知道代表什么就行了,就对应上面源码中红黑树的前两个模板参数嘛。
当然源码里面红黑树不止这两个模板参数,但我们不用管,有的我们不需要加,有的有需要的话我们后面再加。

3.2 map、set的结构定义

那然后我们来创建一个头文件,先把map的结构简单定义一下

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
因为map里面pair的键key不能修改,所以我们加一个const,库里面也是这样搞的。

然后写一下set的:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

3.3 insert的封装

先来看map:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
其实还是复用红黑树的Insert,当然之前我们学过map和set的使用,它们insert的返回值其实是一个pair嘛(当然只是插入一个元素的那个版本),大家可以去看一下文档或复习一些之前文章)
那我们这里先这样写,后面需要的时候再结合具体场景修改。

然后set:

也是一样
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

3.4 insert测试

我们分别对map和set插入一些数据试一试:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

然后呢:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
我们运行程序看起来是正常的(当然我们并没有打印出来看),但是这里面其实是存在问题的。

3.5 发现问题并解决

什么问题呢?

🆗,我们前面实现的红黑树是K模型的,里面的插入就是按照结点里面的data去比较的
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
但是现在map和set都是复用这棵红黑树,所以data里面可能是单独一个key(那就是set实例化的时候),也可能是一个pair(那就对于map)。
那set肯定没问题,因为set对应的就是K模型。
那map的时候呢,map的data里面就是存的pair的键值对,那pair也是可以比较大小,我们直接也说过,所以这里没有报错。
但是pair比较大小的方式是不是不是我们想要的啊。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
而map里面元素的比较我们是不是期望只按照那个key也就是pair.first比较啊

那我们怎么去解决这个问题啊:

那这个地方库里面的做法是比较不错的,我们可以来学习一下
再来看源码里面红黑树的结构
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
大家看第三个参数的作用是啥?(后面那个compare就是控制比较方式,大家有兴趣可以自己后面写一下)
这里参数其实就是为了解决这个问题而引入的,它接收一个仿函数。
我们来实现一下,先来看map,我们就可以这样解决:
写一个仿函数取出pair里面的first
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这个仿函数接收一个pair,返回里面的first。
然后对于set,其实是不需要的,但是为了匹配,因为它们用了同一个红黑树模板,所以也要写一个
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
然后把这个仿函数传给RBTree的第三个模板参数,这样在红黑树里面,我们不需要管data是单独的key还是pair类型的数据,都可以用一个统一的方式拿到真正用了比较的那个单独的key。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那涉及到比较的地方都需要我们改一下,我这里就不全部截图出来了,我最后会把完整的代码放出来。
然后大家可以看一下这个图理解一下
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
🆗,那这个问题我们就解决了

3.6 红黑树迭代器实现

那接下来我们来实现一下迭代器,实现好之后也可以测试一下我们上面插入之后到底对不对。

迭代器我们重点实现红黑树的,map和set直接复用就行了,库里面源码也是这样搞的。

那红黑树的迭代器呢其实就是结点指针的封装,跟我们之前学的list的迭代器差不多:

那在这里我们控制const迭代器以及重载->这个运算符其实用的方法都和之前list一样,通过增加模板参数去控制,大家遗忘的话可以复习一下list那里相关的实现,下面我就直接写了,有些地方就不过多解释了

那我们先来定义一下它的结构:

里面其实就是封装一个结点的指针【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

然后常见的几个成员函数实现一下:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

那红黑树的类模板里面这样写就行了:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
上面这些东西都不难,跟之前学的list里面的是一样的,相信大家都能看懂,我就不解释了。

然后我们写一下迭代器的begin和end吧:

首先说一下我们下面的实现和库里面有所不同,库里面其实稍微麻烦一点,我们后面也会提到,但大家按照我们这里讲的实现就行。
那大家想一下,begin返回的是啥?
🆗,在这里是不是应该返回指向中序遍历第一个结点的迭代器啊。
而中序遍历的第一个结点不就是整棵树最左边的那个结点嘛。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那就是这样【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那end呢?
返回中序遍历最后一个结点的下一个:
那最后一个结点后面就没有元素了啊,所以我们直接用空构造一个迭代器返回就行了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

那const版本的begin和end我们也直接写一下:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

另外这里稍微有点挑战的就是++和- -的重载,这两个重载之后我们就可以使用迭代器遍历红黑树了(后面封装好就是遍历map和set),那我们来讲一下:

其实这里相当于我们要搞出一种新的遍历二叉树的方式,之前我们学了递归和利用栈的非递归的方式。
首先++的重载
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
大家想一下,最开始迭代器it在1这个结点的位置(它是中序遍历第一个嘛),那怎么样让它++就能走到下一个中序遍历的结点上呢?
🆗,其实不论it当前在那个位置,我们都可以认为它在当前所处的这棵子树的根结点上,那么根结点访问完,中序遍历的话下面访问什么?
跟访问完了,是不是要访问右子树啊。
那对于右子树来说,如果它不为空的话,同样要对它进行中序,所以it下面要访问的就是it的右子树的最左结点
当然要注意这里前提是右子树不为空
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那如果是右为空的情况呢?
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
访问完6,然后6的右为空,那下一个是谁?
🆗,大家想,中序遍历是左、根、右嘛。那66访问完了,6的右为空,那是不是相当于6这棵树访问完了。
那6又是1的右子树(左、根、右),那是不是就是1这棵子树访问完了。
然后1的话是它的父亲8的左子树,所以1这棵树访问完就相当于8的左子树访问完了,那下面是不是就要访问根结点8了。
所以如果右为空的话,我们就应该往上去判断it是不是它父亲的左子树,如果不是,就顺着父亲指针往上走,是的话停下来,此时这个父亲就是下一个要访问的结点。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
所以我们这种方法的前提是三叉链,如果不是三叉链的话那就要用我们之前讲的那种借助栈的非递归遍历方法了。
那后续就按照这个逻辑一直走就行了。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那这样最后一个结点27遍历完,27的右为空,判断它和它父亲的关系,不是父亲的左,继续往上走判断,一直走到根结点13也不符合,然后13的父亲是空,就结束了
所以可以认为end在这个位置
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
代码:
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
最后返回一下++之后的迭代器。

然后再来搞一下- -(map和set的迭代器是双向迭代器):

那大家思考一下- -要怎么走?
其实就是跟++的反过来就行了,中序是左子树、根、右子树,那- -就右子树、根、左子树就行了
那这样反过来的话就是先判断it的左子树为不为空,不为空就访问左子树的最右结点,如果左为空,那就要向上去找it是谁的右子树,找到的话当前it的这个父亲就是下一个要访问的结点。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

如果大家去看源码的话会发现它的实现跟我们有一些不同:

他给这棵红黑树增加了一个头结点
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
头结点的左指针指向最左结点(中序第一个),头结点的右指向最右结点,然后它的parent指向根结点,根结点的parent指向这个头结点
然后其它地方其实跟我们的实现是差不多的。【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那他的end就指向这个头结点,就不为空。
大家有兴趣可以看一下它这个实现,但是按我们上面写的就可以了,当然库里面的实现在某些地方会比我们的好一些,我们这样实现的话如果对end–的话其实就会有问题,因为我们的end使用空nullptr构造的,就没法向前寻找前一个结点,因此最好的方式是将end()放在头结点的位置,当然其它位置的是没问题的。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
不过问题不大,我们重点还是学习它的底层框架。

然后反向迭代器大家有兴趣可以自己搞一下,前面我们也讲过怎么弄,这里就不写了。

3.7 封装set和map的迭代器并测试

那红黑树的迭代器实现的差不多了,我们就可以用它封装set和map的迭代器了。

先来搞一下set:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
其实就是在set里面封装一个begin和end(也是去调红黑树的)就行了,set里面的迭代器其实本质就是红黑树迭代器的一个typedef。
那我们运行一下代码看看
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
没有问题,是升序。
再来一组
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
没问题。
那有了迭代器,就可以用范围for了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

那然后我们再来封装一下map的迭代器并测试一下:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这就好了
测试一下
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
没问题。
然后范围for
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
也是可以的。

3.8 map的[]重载

那map与set不同的是不是他还重载了[]啊,这个我们之前在map和set的使用那篇文章也讲过。

那我们接下来就来实现一下map的【】:

那通过前面的学习,我们知道map重载的[],底层的实现是跟insert的返回值有关系的,所以我们得修改一下红黑树的insert
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
应该让它返回一个pair:
当插入成功的时候,pair的first为指向新插入元素的迭代器,second为true,当插入失败的时候(其实就是插入的键已经存在了),那它的first为容器中已存在的那个相同的等效键元素的迭代器,second为false。
所以后面这个bool其实是标识插入成功还是失败

这都是之前讲过的。
改造一下
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

那map和set里面insert的封装我们也要改一下:

其实把返回值改一下就行了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

然后就可以给map重载[]了:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
那这就写好了

我们可以用统计次数那个程序测试一下:

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
没有问题。

3.9 元素可以修改的问题解决

但是我们上面的实现其实还存在一些问题:

通过前面的学习我们知道set里面的元素是不能修改的,因为底层是搜索树,如果可以随意修改的话就会破坏搜索树的关系。
而对于map,key是不可以修改的,不过value可以修改。
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
但我们现在是可以修改的,你看这样他还满足搜索树吗?
就不是了。

那如何解决呢?

我们可以看一下库里面是怎么解决的:
先来看set里面的迭代器
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
我们看到它里面的普通迭代器和const迭代器都是用的红黑树的const迭代器。
那我们也这样写呗
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
但是现在会有报错
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
什么原因呢?
🆗,大家看
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这里_t调用begin,返回的是普通迭代器,但是现在这个返回值iterator是不是红黑树里面const迭代器的typedef,所以这里无法进行类型转换(普通迭代器转换为const迭代器)就出现了问题。
那我们再来看一下库里面怎么解决的:
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
不过我们看到库里面并没有在这里做处理。
那其实我们要来看一下红黑树里面的这个地方:
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
大家看这里红黑树的迭代器的第三个构造函数。
仔细看一下,是拷贝构造吗?
不是的,大家回忆一下拷贝构造,他要求参数必须是当前类同类型对象的引用
而大家看这里:
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
Self才是当前类类型的别名,而第三个构造函数的参数是iterator,跟Self是不一样的,如果它用Self的话那就是拷贝构造了。
这里iterator一定是普通迭代器类型,因为它的后两个参数引用和指针都没有加const,而Self的话如果传过来的Ref和Ptr是普通的引用和指针那它就是普通迭代器,如果是const修饰的那它就是const迭代器。
所以,这里第三个构造函数参数写成iterator的情况下:
如果当前__rb_tree_iterator这个迭代器的类模板被实例化成iterator的话那它就是一个拷贝构造了,因为这是这个参数就跟类对象的类型匹配了;
如果被实例化成这个
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
const_iterator的话,那它就是一个可以用iterator去构造const_iterator的构造函数了。
那它这里支持了普通迭代器构造const迭代器的话我们上面提到的那个问题是不是就解决了,因为单参数的构造函数是可以支持隐式类型转换的,这个我们之前学过的。

那大家可能会想既然不能修改那直接让红黑树只实现const迭代器不就行了?

这样对于set是可行的,但是map也要复用这棵红黑树啊,map的key虽然不支持修改,但是value是可以修改的。
所以map是要区分普通迭代器和const迭代器的,不能像set那样普通迭代器和const迭代器都是用的红黑树的const迭代器。

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set

那现在我们来解决一下:

我们也加一个可以支持普通迭代器构造const迭代器的构造函数就行了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
这样的话不论你__RBTreeIterator被实例化成了普通迭代器还是const迭代器(取决于Ref和Ptr传的是啥),我这里都是用普通迭代器去构造你的(因为参数是<T, T&, T*>
然后再运行
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
就不报错了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
而且现在就不能修改了

那map要如何解决Key不能修改,但是value可以修改的问题呢?

那这个我们其实上面已经提到过了
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
就是传这个模板参数的时候K用const修饰就行了
我们来试一下
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
second即value是可以修改的
【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set
而first即key是不行的。

🆗,那我们的map和set的模拟实现就差不多讲到这里,其它一些我们这里没实现的东西大家有兴趣的可以自己补充,这里我们就不写了。

4. 代码展示

4.1 RBTree.h

#pragma once

enum Colour
{
	RED,
	BLACK,
};

template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _parent;
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	T _data;
	Colour _col;

	RBTreeNode(const T& data)
		:_parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		, _data(data)
		, _col(RED)
	{}

};

template <class T, class Ref, class Ptr>
struct __RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBTreeIterator<T, Ref, Ptr> Self;

	Node* _node;

	__RBTreeIterator(Node* node)
		:_node(node)
	{}

	//不论`__RBTreeIterator`被实例化成了普通迭代器还是const迭代器(取决于Ref和Ptr传的是啥)
	//,我这里都是用普通迭代器去构造的(因为参数是`<T, T&, T*>`)
	__RBTreeIterator(const __RBTreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	Self operator++()
	{
		//如果右不为空,下一个访问的是右子树的最左结点
		if (_node->_right)
		{
			Node* subLeft = _node->_right;
			while (subLeft->_left)
			{
				subLeft = subLeft->_left;
			}
			_node = subLeft;
		}
		//右为空,往上找it是谁的左子树
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

	Self operator--()
	{
		//如果左不为空,下一个访问的是左子树的最右结点
		if (_node->_left)
		{
			Node* subRight = _node->_left;
			while (subRight->_right)
			{
				subRight = subRight->_right;
			}
			_node = subRight;
		}
		//左为空,往上找it是谁的右子树
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}
};

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*> const_iterator;

	iterator begin()
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return iterator(cur);
	}

	iterator end()
	{
		return iterator(nullptr);
	}

	//const版本begin和end
	const_iterator begin()const
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return const_iterator(cur);
	}

	const_iterator end()const
	{
		return const_iterator(nullptr);
	}

	pair<iterator,bool> Insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		KeyOfT kot;
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (kot(data) < kot(cur->_data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kot(data) > kot(cur->_data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return make_pair(iterator(cur), false);
			}
		}
		//走到这里cur为空,就是key应该插入的位置
		cur = new Node(data);
		Node* newNode = cur;

		//链接
		if (kot(data) < kot(parent->_data))
			parent->_left = cur;
		if (kot(data) > kot(parent->_data))
			parent->_right = cur;

		//链接父亲指针
		cur->_parent = parent;

		//如果插入之后它的parent是红的,就需要进行调整
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//如果父亲是祖父的左孩子,那右孩子就是叔叔
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//这里处理的情况是叔叔存在且为红,变色+向上继续处理
				if (uncle && uncle->_col == RED)
				{
					//将p,u改为黑,g改为红
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					//更新cur为grandfather,判断它的父亲是什么情况:
					//1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
					//2.如果它的父亲存在且为红,重新循环进行调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在/叔叔存在且为黑的情况
				{
					//     g
					//   p   u
					// c 
					if (cur == parent->_left)//左左——右单旋+变色
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//左右——左右双旋+变色
					{
						//     g
						//   p   u
						//     c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					//这里调整完就可以break了,因为颜色都变的合适了,而相关的链接关系旋转会帮我们处理
					break;
				}
			}
			//如果父亲是祖父的右孩子,那左孩子就是叔叔
			else //parent = grandfather->_right
			{
				Node* uncle = grandfather->_left;
				//这里处理的情况是叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					//将p,u改为黑,g改为红
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;

					//更新cur为grandfather,判断它的父亲是什么情况:
					//1.如果不存在或者为黑,就需要继续处理了,也不会进行循环了
					//2.如果它的父亲存在且为红,重新循环进行调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)//右右——左单旋+变色
					{
						//    g
						//  u   p
						//        c
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//右左——右左双旋+变色
					{
						//    g
						//  u   p
						//    c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		//上面处理过程中有可能会把根变成红色,这里统一处理一下,把根置成黑
		_root->_col = BLACK;
		return make_pair(iterator(newNode), true);
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool IsBlance()
	{
		if (_root && _root->_col == RED)
		{
			cout << "根结点是红色" << endl;
			return false;
		}

		//先求出一条路径黑色结点数量
		int mark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++mark;
			cur = cur->_left;
		}

		//检查是否出现连续红色结点及所有路径黑色结点数量是否相等
		return _Check(_root, 0, mark);
	}
	int TreeHeight()
	{
		return _TreeHeight(_root);
	}
	Node* find(const T& data)
	{
		KeyOfT kot;
		Node* cur = _root;
		while (cur)
		{
			if (kot(data) < kot(cur->_data))
			{
				cur = cur->_left;
			}
			else if (kot(data) > kot(cur->_data))
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	~RBTree()
	{
		_Destroy(_root);
		_root = nullptr;
	}
private:
	void _Destroy(Node* root)
	{
		if (root == nullptr)
			return;

		_Destroy(root->_left);
		_Destroy(root->_right);
		delete root;
	}
	int _TreeHeight(Node* root)
	{
		if (root == nullptr)
			return 0;
		int RightH = _TreeHeight(root->_left);
		int leftH = _TreeHeight(root->_right);
		return RightH > leftH ? RightH + 1 : leftH + 1;
	}
	bool _Check(Node* root, int blackNum, int mark)
	{
		if (root == nullptr)
		{
			//走到空就是一条路径走完了,比较一下是否相等
			if (blackNum != mark)
			{
				cout << "存在黑色结点数量不相等" << endl;
				return false;
			}
			return true;
		}

		if (root->_col == BLACK)
			++blackNum;

		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "出现连续红色结点" << endl;
			return false;
		}

		return _Check(root->_left, blackNum, mark)
			&& _Check(root->_left, blackNum, mark);
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_data << " ";
		_InOrder(root->_right);
	}
	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//旋转并更新_parent指针
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		//先保存一下parent->_parent,因为下面会改它
		Node* pparent = parent->_parent;

		//旋转并更新_parent指针
		subR->_left = parent;
		parent->_parent = subR;

		//若pparent为空则证明旋转的是一整棵树,因为根结点的_parent为空
		if (pparent == nullptr)
		{
			//subR是新的根
			_root = subR;
			_root->_parent = nullptr;
		}
		//若pparent不为空,则证明旋转的是子树,parent上面还有结点
		else
		{
			//让pparent指向子树旋转之后新的根
			if (pparent->_left == parent)
			{
				pparent->_left = subR;
			}
			else
			{
				pparent->_right = subR;
			}
			//同时也让新的根指向pparent
			subR->_parent = pparent;
		}
	}

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//旋转并更新_parent指针
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		//先保存一下parent->_parent,因为下面会改它
		Node* pparent = parent->_parent;

		//旋转并更新_parent指针
		subL->_right = parent;
		parent->_parent = subL;

		//若parent等于_root则证明旋转的是一整棵树(这也是一种判断方法)
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//让pparent指向子树旋转之后新的根
			if (parent == pparent->_left)
			{
				pparent->_left = subL;
			}
			else
			{
				pparent->_right = subL;
			}
			//同时也让新的根指向pparent
			subL->_parent = pparent;
		}
	}

private:
	Node* _root = nullptr;
};

4.2 Map.h

#pragma once

#include "RBTree.h"

namespace yin
{
	template <class K, class V>
	class map
	{
		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;

		iterator begin()
		{
			return _t.begin();
		}

		iterator end()
		{
			return _t.end();
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
			return ret.first->second;
		}

		pair<iterator,bool> insert(const pair<const K, V>& kv)
		{
			return _t.Insert(kv);
		}
	private:
		RBTree<K, pair<const K, V>, MapKeyOfT> _t;
	};
	void test_map1()
	{
		map<string, string> dict;
		dict.insert(make_pair("sort", "排序"));
		dict.insert(make_pair("left", "左边"));
		dict.insert(make_pair("right", "右边"));
		dict.insert(make_pair("string", "字符串"));
		dict.insert(make_pair("insert", "插入"));
		dict.insert(make_pair("erase", "删除"));
		dict.insert(make_pair("erase", "删除"));


		map<string, string>::iterator it = dict.begin();
		while (it != dict.end())
		{
			cout << it->first << ":" << it->second << endl;
			++it;
		}
		cout << endl;

		/*for (auto e : dict)
		{
			cout << e.first << ":" << e.second << endl;
		}*/
	}
	void test_map2()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };

		map<string, int> m;
		for (auto e : arr)
		{
			m[e]++;
		}

		for (auto e : m)
		{
			cout << e.first << ":" << e.second << endl;
		}
	}
}

4.3 Set.h

#pragma once

#include "RBTree.h"

namespace yin
{
	template <class K>
	class set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;

		iterator begin()
		{
			return _t.begin();
		}

		iterator end()
		{
			return _t.end();
		}

		pair<iterator,bool> insert(const K& key)
		{
			return _t.Insert(key);
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};

	void test_set1()
	{
		int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
		set<int> s1;
		for (auto e : arr)
		{
			s1.insert(e);
		}

		set<int>::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		/*for (auto e : s1)
		{
			cout << e << " ";
		}*/
		cout << endl;
	}
}

4.4 Test.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "Map.h"
#include "Set.h"
#include <set>

int main()
{
	//yin::test_set1();
	yin::test_map1();
	return 0;
}

【C++】 使用红黑树模拟实现STL中的map与set,C++,c++,开发语言,数据结构,STL,map,set文章来源地址https://www.toymoban.com/news/detail-662412.html

到了这里,关于【C++】 使用红黑树模拟实现STL中的map与set的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】红黑树封装map和set

    我们知道,map和set的底层是红黑树,但是我们这里可能有一个疑问,我们知道,set是K模型的容器,而map是KV模型的容器,那么他们为什么能同样使用红黑树呢?我们来看看STL库中源码是怎样实现的 我们可以看到,stll中map和set头文件分别包含了三个头文件,其中stl_tree.h是红黑

    2024年02月15日
    浏览(39)
  • 用红黑树封装map&set【C++】

    目录 前言 一,定义 二,完善setmap比较  三,迭代器实现 operator++ operator-- operator[] 四,发现问题 解决 修改一: set封装层面 修改二:正向迭代器修改 下期预告: 哈希!! 结语 我们在上篇文章红黑树——插入底层实现【C++】面试重灾区!!-CSDN博客 在上篇文章我们实现了红

    2024年02月06日
    浏览(42)
  • C++ 改造红黑树,封装map和set

    set是Key模型的红黑树 map是Key-Value模型的红黑树 我们之前已经把红黑树实现并测试过了 这是Key-Value模型的红黑树 RBTree.h 要想用红黑树来封装set和map,我们首先想到的是在搞一份Key模型的红黑树, 然后用Key模型红黑树来封装set,Key-Value模型红黑树封装map 但是STL库中set和map的设计却

    2024年03月17日
    浏览(50)
  • 【C++】map与set容器——红黑树底层封装

    💭STL中,容器大概可分为两种类型——序列式容器和关联式容器。在前面的系列文章中,我们已经介绍了诸多序列式容器,如:vector、list、stack、queue等,它们以序列的形式存储数据。 💭而关联式容器也是一种非常重要的容器。标准的STL关联式容器分为set(集合)和map(映

    2023年04月11日
    浏览(39)
  • C++【一棵红黑树封装 set 和 map】

    ✨个人主页: 北 海 🎉所属专栏: C++修行之路 🎃操作环境: Visual Studio 2019 版本 16.11.17 红黑树的基本情况我们已经在上一篇文章中学习过了,本文主要研究的是红黑树的实际应用:封装实现 set 和 map ,看看如何通过一棵红黑树满足两个不同的数据结构;在正式封装之前,

    2024年02月11日
    浏览(40)
  • C++ | 红黑树以及map与set的封装

    目录 前言 一、红黑树 1、红黑树的基本概念 2、红黑树相关特性 3、红黑树结点的定义 4、红黑树的查找 5、红黑树的插入 6、二叉树的拷贝构造与析构 7、红黑树的检测 8、红黑树总结 二、map与set的封装 1、红黑树的结点 2、红黑树迭代器 3、set的封装 4、map的封装 三、源码仓库

    2024年02月14日
    浏览(37)
  • 【C++】用红黑树迭代器封装map和set

    封装有点难 - . - 文章目录 前言 一、红黑树原先代码的修改 二、红黑树迭代器的实现 总结 因为我们要将红黑树封装让map和set使用,所以我们要在原来的基础上将红黑树代码进行修改,最主要的是修改模板参数,下面我们直接进入正题: 首先我们拿出STL中的源代码,看看大佬

    2024年02月06日
    浏览(42)
  • 【ONE·C++ || set和map(二):AVL树和红黑树】

      主要介绍AVL树和红黑树的部分框架结构和特性             1)、AVL树是什么   AVL树实际是对二叉搜索树的特化,又被称为高度平衡二叉搜索树。    二叉搜索树问题说明: 数据有序或接近有序时,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索

    2024年02月03日
    浏览(45)
  • 【C++】用一棵红黑树同时封装出map和set

    苦厄难夺凌云志,不死终有出头日。 1. 在源码里面,对于map和set的实现,底层是用同一棵红黑树封装出来的,并不是用了两棵红黑树,一个红黑树结点存key,一个红黑树结点存key,value的键值对,这样的方式太不符合大佬的水准了,实际上在红黑树底层中用了一个模板参数Va

    2023年04月13日
    浏览(43)
  • Learning C++ No.23【红黑树封装set和map】

    北京时间:2023/5/17/22:19,不知道是以前学的不够扎实,还是很久没有学习相关知识,对有的知识可以说是遗忘了许多,以该篇博客有关知识为例,我发现我对迭代器和模板的有关知识的理解还不够透彻,不知道是对以前知识的遗忘,还是现在所学确实有难度,反正导致我很懵

    2024年02月05日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包