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

文章目录

  • 前言
  • 1. 对之前实现的红黑树进行一些补充和完善
    • 1.1 析构
    • 1.2 查找
  • 2. STL源码中map和set的实现
  • 3. 改造红黑树+封装map和set
    • 3.1 红黑树结构修改
    • 3.2 map、set的结构定义
    • 3.3 insert的封装
    • 3.4 insert测试
    • 3.5 发现问题并解决
    • 3.6 红黑树迭代器实现
    • 3.7 封装set和map的迭代器并测试
    • 3.8 map的[]重载
    • 3.9 元素可以修改的问题解决
  • 4. 代码展示
    • 4.1 RBTree.h
    • 4.2 Map.h
    • 4.3 Set.h
    • 4.4 Test.cpp

前言

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

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

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

1.1 析构

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

我们来实现一下:

二叉树的销毁我们之前也有讲过,走一个后序遍历销毁就行了
在这里插入图片描述
代码很简单,我就不解释了

1.2 查找

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

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

直接上代码
在这里插入图片描述

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

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

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

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

然后我们再来看一下set:

在这里插入图片描述
首先set的底层也是红黑树,这没什么说到,我们都知道,但是我们会发现map和set底层用的是同一个红黑树的类模板。
但是它们不是一个对应KV模型,一个对应K模型嘛,那我们想的可能是应该实现两个红黑树,一棵存Key的单个值,另一棵存KV的键值对,然后用这两棵红黑树分别封装map和set。

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

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

在这里插入图片描述
set的里面虽然存的是单个值,可它除了有key_type之外,也有value_type,但是set里面的key_type和value_type都是Key的typedef,所以相当于传来两个key。
当然从表面上来看他之所以这样做的原因是他和map用的是同一个红黑树的类模板,而这个类模板是有两个参数的,所以它必须传两个。

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

在这里插入图片描述
这里这个node_count其实就是记录了结点的数量,这个map和set需要获取size的时候就很方便,不需要再去遍历计数了。
然后下面这个link_type这个类型其实就是结点的指针,这个header代表啥我们可以先不管。
然后我们看到红黑树这里的模板参数是固定的,前两个模板参数是key和value,而红黑树结点里面放的其实就是value
在这里插入图片描述

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

在这里插入图片描述
这样如果是set,那红黑树的结点里面存的就是key,如果是map,那红黑树的结点里面存的就是pair的键值对。
所以这里第二个模板参数value接收的什么,就决定了红黑树结点里面存的是什么类型的数据。

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

在这里插入图片描述
大家想一下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 红黑树结构修改

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

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

在这里插入图片描述
在这里插入图片描述
我们这里就还用我们直接写到就可以,只是实现方式不同,这里没什么影响。

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

因为我们当时是按照K模型实现的,只有一个模板参数在这里插入图片描述
所以要加一个,至于这里为什么需要两个上面已经解释过了
在这里插入图片描述
这里我们就用KT,大家知道代表什么就行了,就对应上面源码中红黑树的前两个模板参数嘛。
当然源码里面红黑树不止这两个模板参数,但我们不用管,有的我们不需要加,有的有需要的话我们后面再加。

3.2 map、set的结构定义

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

在这里插入图片描述
因为map里面pair的键key不能修改,所以我们加一个const,库里面也是这样搞的。

然后写一下set的:

在这里插入图片描述

3.3 insert的封装

先来看map:

在这里插入图片描述
其实还是复用红黑树的Insert,当然之前我们学过map和set的使用,它们insert的返回值其实是一个pair嘛(当然只是插入一个元素的那个版本),大家可以去看一下文档或复习一些之前文章)
那我们这里先这样写,后面需要的时候再结合具体场景修改。

然后set:

也是一样
在这里插入图片描述

3.4 insert测试

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

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

然后呢:

在这里插入图片描述
我们运行程序看起来是正常的(当然我们并没有打印出来看),但是这里面其实是存在问题的。

3.5 发现问题并解决

什么问题呢?

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

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

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

3.6 红黑树迭代器实现

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

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

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

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

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

里面其实就是封装一个结点的指针在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述
上面这些东西都不难,跟之前学的list里面的是一样的,相信大家都能看懂,我就不解释了。

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

首先说一下我们下面的实现和库里面有所不同,库里面其实稍微麻烦一点,我们后面也会提到,但大家按照我们这里讲的实现就行。
那大家想一下,begin返回的是啥?
🆗,在这里是不是应该返回指向中序遍历第一个结点的迭代器啊。
而中序遍历的第一个结点不就是整棵树最左边的那个结点嘛。
在这里插入图片描述
那就是这样在这里插入图片描述
那end呢?
返回中序遍历最后一个结点的下一个:
那最后一个结点后面就没有元素了啊,所以我们直接用空构造一个迭代器返回就行了
在这里插入图片描述

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

在这里插入图片描述

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

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

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

那大家思考一下- -要怎么走?
其实就是跟++的反过来就行了,中序是左子树、根、右子树,那- -就右子树、根、左子树就行了
那这样反过来的话就是先判断it的左子树为不为空,不为空就访问左子树的最右结点,如果左为空,那就要向上去找it是谁的右子树,找到的话当前it的这个父亲就是下一个要访问的结点。
在这里插入图片描述

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

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

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

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

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

先来搞一下set:

在这里插入图片描述
其实就是在set里面封装一个begin和end(也是去调红黑树的)就行了,set里面的迭代器其实本质就是红黑树迭代器的一个typedef。
那我们运行一下代码看看
在这里插入图片描述
没有问题,是升序。
再来一组
在这里插入图片描述
没问题。
那有了迭代器,就可以用范围for了
在这里插入图片描述

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

在这里插入图片描述
这就好了
测试一下
在这里插入图片描述
没问题。
然后范围for
在这里插入图片描述
也是可以的。

3.8 map的[]重载

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

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

那通过前面的学习,我们知道map重载的[],底层的实现是跟insert的返回值有关系的,所以我们得修改一下红黑树的insert
在这里插入图片描述
应该让它返回一个pair:
当插入成功的时候,pair的first为指向新插入元素的迭代器,second为true,当插入失败的时候(其实就是插入的键已经存在了),那它的first为容器中已存在的那个相同的等效键元素的迭代器,second为false。
所以后面这个bool其实是标识插入成功还是失败

这都是之前讲过的。
改造一下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

其实把返回值改一下就行了
在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
那这就写好了

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

在这里插入图片描述
没有问题。

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

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

通过前面的学习我们知道set里面的元素是不能修改的,因为底层是搜索树,如果可以随意修改的话就会破坏搜索树的关系。
而对于map,key是不可以修改的,不过value可以修改。
在这里插入图片描述
但我们现在是可以修改的,你看这样他还满足搜索树吗?
就不是了。

那如何解决呢?

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

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

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

在这里插入图片描述

那现在我们来解决一下:

我们也加一个可以支持普通迭代器构造const迭代器的构造函数就行了
在这里插入图片描述
这样的话不论你__RBTreeIterator被实例化成了普通迭代器还是const迭代器(取决于Ref和Ptr传的是啥),我这里都是用普通迭代器去构造你的(因为参数是<T, T&, T*>
然后再运行
在这里插入图片描述
就不报错了
在这里插入图片描述
而且现在就不能修改了

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

那这个我们其实上面已经提到过了
在这里插入图片描述
就是传这个模板参数的时候K用const修饰就行了
我们来试一下
在这里插入图片描述
second即value是可以修改的
在这里插入图片描述
而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;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/83545.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

svg图片如何渲染到页面,以及svg文件的上传

svg图片渲染到页面的几种方式 背景&#x1f7e1;require.context获取目录下的所有文件&#x1f7e1;方式1: 直接在html中渲染&#x1f7e1;方式: 发起ajax请求&#xff0c;获取SVG文件 背景 需要实现从本地目录下去获取所有的svg图标进行预览&#xff0c;将选中的图片显示在另…

报道|新鲜出炉!INFORMS公布六位新任期刊主编

推文作者&#xff1a;徐思坤 编者按 INFORMS旗下的六本期刊&#xff0c;Management Science、Operations Research、Service Science、Tutorials in OR、INFORMS Analytics Collection&#xff0c;以及Transportation Science的新任主编公布&#xff0c;并将于2024年1月1日正式…

基于微信小程序的上门维修评价系统_22c7h-

随着科学研究的不断深入&#xff0c;有关上门维修的各种信息量也在成倍增长。面对庞大的信息量&#xff0c;就需要有上门维修系统来提高管理工作的效率。通过这样的系统&#xff0c;我们可以做到信息的规范管理和快速查询&#xff0c;从而减少了管理方面的工作量。 建立基于微信…

CentOS ens160 显示disconnected

使用nmcli device查看网卡状态&#xff0c;显示如图&#xff1a; 检查宿主机系统VMware DHCP Sevice和VMware NAT Sevice服务是否正常运行。 右键点击我的电脑管理按钮&#xff0c;打开计算机管理点击服务

MyBatis的基本入门及Idea搭建MyBatis坏境且如何一步骤实现增删改查(CRUD)---详细介绍

一&#xff0c;MaBatis是什么&#xff1f; 首先是一个开源的Java持久化框架&#xff0c;它可以帮助开发人员简化数据库访问的过程并提供了一种将SQL语句与Java代码进行解耦的方式&#xff0c;使得开发人员可以更加灵活地进行数据库操作。 1.1 Mabatis 受欢迎的点 MyBatis不仅是…

用于优化开关性能的集成异质结二极管的4H-SiC沟道MOSFET

标题&#xff1a;4H-SiC Trench MOSFET with Integrated Heterojunction Diode for Optimizing Switching Performance 摘要 本研究提出了一种新型的4H-SiC沟道MOSFET&#xff0c;其在栅槽底部集成了异质结二极管&#xff08;HJD-TMOS&#xff09;&#xff0c;并通过TCAD模拟进…

【vim 学习系列文章 5 - cscope 过滤掉某些目录】

文章目录 cscope 过滤目录介绍 cscope 过滤目录介绍 第一步创建自己的cscope脚本~/.local/bin/cscope.sh&#xff0c;如下&#xff1a; function my_cscope() {CODE_PATHpwdecho "$CODE_PATH"echo "start cscope...."if [ ! -f "$CODE_PATH/cscope.…

编织梦想:SpringBoot AOP 教程与自定义日志切面完整实战

什么是 AOP AOP 是指通过预编译方式和运行期动态代理的方式&#xff0c;在不修改源代码的情况下对程序进行功能增强的一种技术。AOP 不是面向对象编程&#xff08;OOP&#xff09;的替代品&#xff0c;而是 OOP 的补充和扩展。它是一个新的维度&#xff0c;用来表达横切问题&a…

STM32 中断复习

中断 打断CPU执行正常的程序&#xff0c;转而处理紧急程序&#xff0c;然后返回原暂停的程序继续运行&#xff0c;就叫中断。 在确定时间内对相应事件作出响应&#xff0c;如&#xff1a;温度监控&#xff08;定时器中断&#xff09;。故障处理&#xff0c;检测到故障&#x…

用python从零开始做一个最简单的小说爬虫带GUI界面(1/3)

目录 前言 三节博客内容概要 PyQt5的配置 设置软件的快捷启动方式 1. 用于设计界面的程序 2. 将Qt Designer设计出来的ui文件转化为py文件 3. 可以把py文件打包成可执行的exe文件 4. 将ico图片放在qrc文件中&#xff0c;再将qrc文件转换成py…

SpringBoot中properties、yml、yaml的优先级

原理 配置优先级低的会先加载然后会被配置优先级高的覆盖 验证 创建SpringBoot项目&#xff08;网址&#xff09; 在resource目录下创建application.properties、application.yml、application.yaml文件 运行 结论 优先级顺序&#xff1a; properties>yml>yaml

安防监控视频云存储平台EasyNVR出现内核报错的情况该如何解决?

安防视频监控汇聚EasyNVR视频集中存储平台&#xff0c;是基于RTSP/Onvif协议的安防视频平台&#xff0c;可支持将接入的视频流进行全平台、全终端分发&#xff0c;分发的视频流包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等格式。 近期有用户联系到我们&#xff0c;EasyNVR…

Shell语法揭秘:深入探讨常见Linux Shell之间的语法转换

深入探讨常见Linux Shell之间的语法转换 一、引言二、Linux常用Shell&#xff1a;Bash、Zsh、Ksh、Csh、Tcsh和Fish的简介2.1、Bash、Zsh、Ksh、Csh、Tcsh和Fish的特点和用途2.2、语法差异是常见Shell之间的主要区别 三、变量和环境设置的语法差异3.1、变量定义和使用的不同语法…

MAC钓鱼并Root权限上线CS并权限维持,以及所有的坑如何解决

本文转载于&#xff1a;https://www.freebuf.com/articles/web/350592.html 作者&#xff1a;文鸯涂鸦智能安全实验室 制作MAC 一、下载工具 首先从github上下载CrossC2。链接&#xff1a;https://github.com/gloxec/CrossC2/releases/tag/v3.1.0。 根据你CS客户端的操作系统选…

Linux内核学习(六)—— 中断(基于Linux 2.6内核)

一、中断 中断使得硬件得以发出通知给处理器。中断随时都可以产生&#xff0c;如键盘敲击就会触发中断&#xff0c;通知操作系统有按键按下。 不同设备对应的中断不同&#xff0c;而每个中断都通过一个唯一的数字标识。这些中断值通常被称为中断请求&#xff08;IRQ&#xff…

Dockerfile制作镜像与搭建LAMP环境

1、编写Dockerfile制作Web应用系统nginx镜像&#xff0c;生成镜像nginx:v1.1&#xff0c;并推送其到私有仓库。 具体要求如下&#xff1a; &#xff08;1&#xff09;基于centos基础镜像&#xff1b; &#xff08;2&#xff09;指定作者信息&#xff1b; &#xff08;3&#x…

VMware虚拟机Ubuntu无法连接网络的解决方法

一、解决办法 网络适配器设置 终端依次执行下面命令即可 sudo nmcli networking off sudo nmcli networking onsudo service network-manager start #或者 sudo service NetworkManager start成功出现这个图标&#xff0c;即代表网络连接成功。

AveMaria 传播手段的变化

AveMaria 是一种最早在 2018 年 12 月出现的窃密木马&#xff0c;攻击者越来越喜欢使用其进行攻击&#xff0c;运营方也一直在持续更新和升级。在过去六个月中&#xff0c;研究人员观察到 AveMaria 的传播手段发生了许多变化。 2022 年 12 月攻击行动 研究人员发现了名为 .Vh…

Linux中shell脚本——for、while循环及脚本练习

目录 一.for循环 1.1.基本格式 1.2.类C语言格式 二.while循环 2.1.基本格式 2.2.死循环语句 三.跳出循环 3.1.continue跳出循环 3.2.break跳出循环 四.常用循环 4.1.循环打印九九乘法表 4.2.循环ping测试某个网段网络连通性 4.3.while死循环实现猜数字游戏 4.4.数…

Python自动化小技巧18——自动化资产月报(word设置字体表格样式,查找替换文字)

案例背景 每月都要写各种月报&#xff0c;经营管理月报&#xff0c;资产月报.....这些报告文字目标都是高度相似的&#xff0c;只是需要替换为每个月的实际数据就行&#xff0c;如下&#xff1a; (打码是怕信息泄露.....) 可以看到&#xff0c;这个报告的都是高度模板化&…