[C++]AVL树、红黑树以及map、set封装

news/2024/2/27 21:45:38

目录

前言:

1 AVL树

1.1 AVL树的概念

1.2 AVL树结点的定义

1.3 AVL树插入

1.4 插入结点的调整

1.5 AVL树的旋转调整

1.5.1 右单旋

1.5.2 左单旋

1.5.3 左右双旋

1.5.4 右左双旋

1.5.4种旋转的判断方式

2 红黑树

2.1 红黑树概念

2.2 红黑树与AVL树的比较

2.3 红黑树结点结构

2.4 红黑树普通二叉搜索树插入

2.5 红黑树的调整

2.5.1 当前结点为红、parent为红、grandfather为黑、uncle为红色

2.5.2 cur为红、p为红、g为黑、叔叔不存在或则存在为黑色,cur、p、g在同一条线上

2.5.3 cur为红,p为红,g为黑,u不存在或则u存在为黑,cur与p和g是折线关系

 2.6 红黑树插入代码

3 map和set的封装

3.1 模板参数介绍

3.2 迭代器的实现

3.2.1 迭代器++操作

3.2.2 迭代器--操作

3.2.3 红黑树内迭代器访问操作

3.3 map的封装

3.4 set的封装

3.5 map和set封装优化

3.5.1 set的优化

 3.5.2 map的优化

4 map和set的完整实现代码

4.1 map

4.2 set代码

4.3 红黑树代码


前言:

        本篇基于上一篇内容普通二叉搜索树展开,并通过两种方式优化它的插入函数,最后通过红黑树作为map和set的底层封装。

        注:本篇有一定的难度,博主不确定大家能直接看明白,建议上手实现。


        相信大家如果看了我的上篇内容的最后总结,那么一定知道,当一颗搜索树的左右子树高度差十分大的时候,就会导致一个搜索效率低下的问题,那么以下的两颗树结构就是为了优化这一问题的出现。

1 AVL树

1.1 AVL树的概念

它的左右子树都是AVL

左右子树高度之差 ( 简称平衡因子 ) 的绝对值不超过 1(-1/0/1)

        什么意思呢?很简单,那就是一颗完整没有错误的AVL树,不仅仅是根节点是一颗AVL树,它的左右子树同样满足是AVL树这一概念,这满足我们递归划分子问题的需求。第二点的左右子树高度只差的绝对值不超过1,也就表示了任何一个结点的左子树与右子树的深度相差,最大只能有1层的差距。为什么是一层呢?不能是0层?

        这个问题没啥营养,毕竟如果上一次高度差为0的满二叉树,那么下一次应该怎么插入 呢?毕竟无论如何插入都会让某字节的高度差变化。

        第二个概念里面还有一个平衡因子这个关键字,那么平衡因子是个什么?这里博主给大家简单讲解以下,他就是用来控制我们的AVL树平衡的关键,每一次插入都与它脱离不了关系。当然平衡因子只是实现AVL树的一种方式,还有其它的方式,但是博主也不会。

        如下图就是一颗完整的AVL树,不仅满足搜索树的特性,而且还能保证子树高度差均衡。结点之外的数字表示平衡因子。

1.2 AVL树结点的定义

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K,V>* _left;AVLTreeNode<K,V>* _right;AVLTreeNode<K,V>* _parent;pair<K,V> _kv;int _bf;AVLTreeNode():_left(nullptr), _right(nullptr), _parent(nullptr), _kv(make_pair(K(),V())), _bf(0){}AVLTreeNode(const pair<K,V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};

        博主的AVL树采用了三叉链的形式,及左右子树结点指针和父节点指针,里面存储数据的结构通过pair(也是一个数据结构,一个是key值一个是val值)实现,添加一个_bf变量存储平衡因子。

        平衡因子的改变根据右子树高度减去左子树高度得到。

        该节点博主实现了两个构造函数,但是实际上只需要实现一种有参构造即可,毕竟一颗搜索树不添加数据本身就是很奇怪的事情。

        由于博主不打算利用AVL树封装map和set,所以这里固定了只能实现K、V的结构。

        博主的三叉链形式是实现AVL树的一种简单方式,大家也可以试着写出二叉链的AVL树,只需要添加额外的容器就能实现,例如通过栈模拟递归就是一种方式。

1.3 AVL树插入

bool insert(const pair<K,V>& kv)
{//第一次插入if (_root == nullptr){_root = new Node(kv);return true;}//后续插入Node* cur = _root;Node* parent = nullptr;while (cur){//大于if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}//小于else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}//重复插入else{return false;}}//连接结点cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;
}

        AVL树的插入部分与普通的搜索二叉树没有任何的区别,都是通过判断插入数据与当前结点的大小关系,然后走左子树或则右子树的方式,有问题的小伙伴可以去看博主的上一篇博客。

普通搜索二叉树

1.4 插入结点的调整

        既然作为一颗AVL树,那么插入时肯定是需要对插入后的结点做出调整才行,如果只是简单的和插入,就没有资格被叫做AVL树咯。

        那么首先我们可以想象到,根据搜索树规则,插入完成一颗结点,那么当前结点会影响到那些结点呢?看下图:

         由图可以很清晰的看出来,当我们插入一个结点的时候,受到影响的只有它这一条路径上的结点,并且它自己的平衡因子并不会有任何的改变,毕竟他就是插入的哪一个结点,也不可能有另外的结点在它身上。

        那么我们可以通过什么样的方式去调整平衡因子呢?答案很简单,那就是从当前结点开始,通过三叉链中父节点的指针往回移动,然后修改平衡因子,如果插入结点等于父节点的右侧,那么父节点平衡因子加一,反之则减一

        还有就是有部分结点插入有两种情况,那就是从0变为1或则-1,也可能是从1、-1变为0,那么请问什么情况我们需要继续往上继续修改判断,那种情况我们能够直接表示插入成功了呢?

         上图8号结点的插入就导致了9号结点的平衡因子从1变为了0,但是9号结点的变化引起了7号结点的变化了吗?没有,所以这个时候我们就可以大胆的表示插入成功了

         在看到我们这一次的结点插入,有什么问题?那就是将9号结点的平衡因子从0变为了1,然后7号也跟着变为了1,5号也从-1变为了0,由于5号没有父节点了,所以结束,那么这就表示了,如果当前结点的平衡因子从0变为了1或则-1都需要继续向上更改比较变化

         那么就会有下面的代码:

//平衡因子的调整
while (parent)	//需要不断地向上判断,因为一个结点插入可能会影响整个祖宗路径
{if (parent->_right == cur){parent->_bf++;}else{parent->_bf--;}//由0变1、-1,需要继续调整if (parent->_bf == 1 || parent->_bf == -1){parent = parent->_parent;cur = cur->_parent;}//由1、-1变为0,让整个树变得更加平衡,不需要继续调整else if (parent->_bf == 0){break;}//发生了平衡差过大,需要调整else if (parent->_bf == 2 || parent->_bf == -2){//****************************************//需要调整//****************************************}else{cout << "出现了预期之外的插入错误" << endl;assert(nullptr);}
}

        可以看到我们的代码当中,首先调整是一整个循环体,在进入循环之后,对父节点的平衡因子根据插入结点对于父节点的位置做出++、--的操作,然后判断当前父节点平衡因子的合理性,如果是0,就代表了之前一定是1或则-1,这个时候就不再需要调整了,直接退出即可。

        如果父节点的平衡因子是1或则-1,则表示原来这个结点的平衡因子是0,这一次插入打破了平衡,需要向上继续判断。那么父节点和当前结点分别指向它们各自的父节点。也就是指针的移动。

        然后,如果我们的父节点的平衡因子变为了2或则是-2就需要做出调整了,因为这一次的插入已经导致了我们的树不再是AVL树了,需要通过其它方式改变结构,让它重新变为一颗AVL树。调整这一部分博主放到下一部分为大家讲解。

        平衡因子的变化范围只有-2到2,但是如果程序当中出现了平衡因子变为了其它数据,就表示当前的树结构已经出现的严重的问题,表示我们写的结构出现了问题,需要从代码方面检查错误了。所以直接报错即可。

1.5 AVL树的旋转调整

        接着上一节,当父节点的平衡因子出现2或则-2的情况,就表示了我们的树需要进行旋转调整了,单纯的变化平衡因子已经没有任何作用了。

        旋转调整有4种情况,分别是右单旋,左单旋,右左双旋,左右双旋,下面我将会分别介绍这几种旋转的使用位置。

1.5.1 右单旋

         当我们插入一个结点之后,它影响到了某一个结点的平衡因子变为了-2,而且这个结点的左子树的平衡因子是-1的时候,就代表我们的更改方式是右单旋,可以看到,当我们将abc的高度变为0,就表示了如下的结构:

         对于这样的情况我们应该如何调整呢?相信大家看到这一张图也是能够很容易想到它的调整方式的,那就是把30作为父节点,15和60分别作它的左右子树。也就是如下图:

         很简单吧,不过这样做还有一些小细节需要做,毕竟我们需要调整的子树并不只是这样的3个结点的树,它们有更复杂的结构。就比如我们的高度h不为0,而是1、2、3等更多呢?

         如上图所示,如果出现了需要右单旋的情况,那么我们需要将30的与它的右子树断开连接,把60和它的左子树断开连接,连接时因为60的大于30,所以可以让60做30的右子树,因为30原来的右子树比60小比30大,所以可以做60的新左子树,这个时候连接就成功了,但是平衡因子不对,所以需要对其进行调整。根据树的结构,我们可以直接判断出来,会变化的平衡因子只有30和60结点,通过最终的结构图,可以直接得出都为0

代码:

//右单旋
void right_rotation(Node* parent)
{Node* SubL = parent->_left;Node* SubLR = SubL->_right;Node* ppNode = parent->_parent;//向下连接SubL->_right = parent;parent->_left = SubLR;//向上连接if (SubLR)SubLR->_parent = parent;parent->_parent = SubL;//根的连接if (ppNode == nullptr){_root = SubL;SubL->_parent = nullptr;}//根不为空else{//匹配结点位置if (ppNode->_left == parent){ppNode->_left = SubL;}else{ppNode->_right = SubL;}//向上连接SubL->_parent = ppNode;}//更新平衡因子SubL->_bf = 0;parent->_bf = 0;
}

        上面的代码当中有部分细节,那就是当我们的SubRL为空,也就是图中b为空的时候,是不能向上连接的,需要加一个判断,然后又因为原来的根结点不一定是整棵树的根,所以需要根据原来的对应关系重新建立连接。如果原来的根节点就是根,那么需要替换这颗树的根。最后调整完成之后再更改平衡因子。到这里,右单旋就结束了。

1.5.2 左单旋

        既然有右单旋,那么必定的就有左单旋,但是它们的结构出现得十分相似,那么这里博主就简单的讲解了。

         首先看到上图,当c这个位置插入一个数据导致了30的平衡因子变为了2,60这个位置变为了1,那么就代表了这个时候需要进行左单旋操作。

                 与右单旋的操作方式完全一致,只是方向相反,博主不想重复解释。

代码:

//右高,左单旋调整
void left_rotation(Node* parent)
{Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* ppNode = parent->_parent;//连接SubR->_left = parent;parent->_right = SubRL;//调整与父亲结点的连接关系if (SubRL)SubRL->_parent = parent;parent->_parent = SubR;//调整根节点的关系if (ppNode == nullptr){_root = SubR;_root->_parent = nullptr;}else{//与上继续进行连接,需要判断其左右关系if (ppNode->_left == parent){ppNode->_left = SubR;}else{ppNode->_right = SubR;}SubR->_parent = ppNode;}parent->_bf = SubR->_bf = 0;
}

1.5.3 左右双旋

        通过名字相信大家也能够直接判断出来,这种情况的出现需要旋转两次才能成功的降高度。那么什么样的情况才需要两次旋转呢?

         上图的这种插入方式会导致90号结点重-1变为-2,然后又让30号结点从0变为了1,就表示需要左右双旋了,有朋友可能会认为这个结构和右单旋差不太多啊?,但是事实上是这样嘛?我们来看看:

         请看右单旋之后解决问题了吗?很明显没有嘛,只不过把错误的情况从左边移动到了右边而已,这很明显不是我们想要的结果哇,所以还是需要分析。

        首先,我们的单旋能够解决问题结点的位置是在当前树的最左边或则是最右边,但是我们插入的结点在60的下面,而单旋不会影响到60这个位置的结点,所以我们要把问题移动到30的左边去。所以就需要先对60这个结点进行左单旋,让问题出在a这个位置上去。

         通过对30这个位置进行左单旋,可以看到,30的左子树一定是导致问题的这个地方,然后因为我们插入结点的位置不一定是b下面,也有可能是c下面,也有可能60就是新插入的结点,所以旋转之后它们不一定是导致错误的原因,但是30的左子树一定在旋转之后会出现错误,所以这个时候就回到了右单旋的那个步骤,如下:

         这个时候,请看我们的结构还正确吗?正确的,所以这种情况两次旋转就会出现正确的树结构。当然还有另外两种情况,我一并为大家画出来。

         可以看到,根据不同的插入方式,我们采取了相同的应对方式,那就是先左旋再右旋,都会出现这种结构,唯一不同的那就是平衡因子的不同,相信大家也能看出来它们之间的区别。

        这个平衡因子可以看到,只有这三个结点会有变化,所以我们手动为它做出更改就行。

代码:

//左右双旋
void left_right_rotation(Node* parent)
{Node* SubL = parent->_left;Node* SubLR = SubL->_right;int bf = SubLR->_bf;left_rotation(parent->_left);right_rotation(parent);if (bf == 1){SubL->_bf = -1;parent->_bf = 0;SubLR->_bf = 0;}else if (bf == -1){SubL->_bf = 0;parent->_bf = 1;SubLR->_bf = 0;}else if (bf == 0){SubL->_bf = 0;parent->_bf = 0;SubLR->_bf = 0;}else{cout << "左右旋更新平衡因子出现问题" << endl;assert(nullptr);}
}

        从代码可以看到,我们先对30进行了左旋,然后对90进行了右旋,然后根据60这个结点的平衡因子确定插入的位置到底是哪里。然后根据上图手动为结点更改平衡因子。

1.5.4 右左双旋

         同样的,博主对于这个部分不对多讲解,左右双旋已经够详细了。

        如下所示:

         当我们插入一个结点之后一种情况导致60变为了1,90变为了-1,30变为了2,那么30和60的正负相反,且30为正,那么这个时候就需要右左双旋了旋转后如下:

         根据逻辑分析和画图显示,只有上面的3种情况,所以代码如下:

代码:

//右左双旋
void right_left_rotation(Node* parent)
{Node* SubR = parent->_right;Node* SubRL = SubR->_left;int bf = SubRL->_bf;right_rotation(parent->_right);left_rotation(parent);if (bf == -1){SubR->_bf = 1;SubRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){SubR->_bf = 0;SubRL->_bf = 0;parent->_bf = -1;}else if (bf == 0){SubR->_bf = 0;SubRL->_bf = 0;parent->_bf = 0;}else{cout << "右左旋更新平衡因子出现问题" << endl;assert(nullptr);}
}

1.5.4种旋转的判断方式

//左单旋和左双旋
if (parent->_bf == 2)
{//左单旋if (cur->_bf == 1){left_rotation(parent);}//右左双旋else{right_left_rotation(parent);}
}
else
{//右单旋if (cur->_bf == -1){right_rotation(parent);}//左右双旋else{left_right_rotation(parent);}
}//遇到了应该调整的结点,并调整之后就不需要继续向上查找了
//以后的结点一定是一个正确的AVL树,因为调整的方式就是将该
//位置的树变成高度和以前一样的模式
break;

        根据我之前的画图讲解当中,相信大家也能知道如何判别这几种旋转的方式,博主就不再赘述了。

1.5.6 AVL树插入完整代码:

        对于我们来说,没有必要完全实现一个完整的AVL树,只需要知道它的实现原理就好了,毕竟对于我们学者来说,知道原理才是更重要的,所以博主并不打算分享AVL树的其它功能了。

template<class K, class V>
struct AVLTreeNode
{AVLTreeNode<K,V>* _left;AVLTreeNode<K,V>* _right;AVLTreeNode<K,V>* _parent;pair<K,V> _kv;int _bf;AVLTreeNode():_left(nullptr), _right(nullptr), _parent(nullptr), _kv(make_pair(K(),V())), _bf(0){}AVLTreeNode(const pair<K,V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}
};template<class K,class V>
class AVLTree
{typedef AVLTreeNode<K,V> Node;
public:AVLTree():_root(nullptr){}//插入bool insert(const pair<K,V>& kv){//第一次插入if (_root == nullptr){_root = new Node(kv);return true;}//后续插入Node* cur = _root;Node* parent = nullptr;while (cur){//大于if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}//小于else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}//重复插入else{return false;}}//连接结点cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;//平衡因子的调整while (parent)	//需要不断地向上判断,因为一个结点插入可能会影响整个祖宗路径{if (parent->_right == cur){parent->_bf++;}else{parent->_bf--;}//由0变1、-1,需要继续调整if (parent->_bf == 1 || parent->_bf == -1){parent = parent->_parent;cur = cur->_parent;}//由1、-1变为0,让整个树变得更加平衡,不需要继续调整else if (parent->_bf == 0){break;}//发生了平衡差过大,需要调整else if (parent->_bf == 2 || parent->_bf == -2){//左单旋和左双旋if (parent->_bf == 2){//左单旋if (cur->_bf == 1){left_rotation(parent);}//右左双旋else{right_left_rotation(parent);}}else{//右单旋if (cur->_bf == -1){right_rotation(parent);}//左右双旋else{left_right_rotation(parent);}}//遇到了应该调整的结点,并调整之后就不需要继续向上查找了//以后的结点一定是一个正确的AVL树,因为调整的方式就是将该//位置的树变成高度和以前一样的模式break;}else{cout << "出现了预期之外的插入错误" << endl;assert(nullptr);}}return true;}//右高,左单旋调整void left_rotation(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* ppNode = parent->_parent;//连接SubR->_left = parent;parent->_right = SubRL;//调整与父亲结点的连接关系if (SubRL)SubRL->_parent = parent;parent->_parent = SubR;//调整根节点的关系if (ppNode == nullptr){_root = SubR;_root->_parent = nullptr;}else{//与上继续进行连接,需要判断其左右关系if (ppNode->_left == parent){ppNode->_left = SubR;}else{ppNode->_right = SubR;}SubR->_parent = ppNode;}parent->_bf = SubR->_bf = 0;}//右单旋void right_rotation(Node* parent){Node* SubL = parent->_left;Node* SubLR = SubL->_right;Node* ppNode = parent->_parent;//向下连接SubL->_right = parent;parent->_left = SubLR;//向上连接if (SubLR)SubLR->_parent = parent;parent->_parent = SubL;//根的连接if (ppNode == nullptr){_root = SubL;SubL->_parent = nullptr;}//根不为空else{//匹配结点位置if (ppNode->_left == parent){ppNode->_left = SubL;}else{ppNode->_right = SubL;}//向上连接SubL->_parent = ppNode;}//更新平衡因子SubL->_bf = 0;parent->_bf = 0;}//左右双旋void left_right_rotation(Node* parent){Node* SubL = parent->_left;Node* SubLR = SubL->_right;int bf = SubLR->_bf;left_rotation(parent->_left);right_rotation(parent);if (bf == 1){SubL->_bf = -1;parent->_bf = 0;SubLR->_bf = 0;}else if (bf == -1){SubL->_bf = 0;parent->_bf = 1;SubLR->_bf = 0;}else if (bf == 0){SubL->_bf = 0;parent->_bf = 0;SubLR->_bf = 0;}else{cout << "左右旋更新平衡因子出现问题" << endl;assert(nullptr);}}//右左双旋void right_left_rotation(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;int bf = SubRL->_bf;right_rotation(parent->_right);left_rotation(parent);if (bf == -1){SubR->_bf = 1;SubRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){SubR->_bf = 0;SubRL->_bf = 0;parent->_bf = -1;}else if (bf == 0){SubR->_bf = 0;SubRL->_bf = 0;parent->_bf = 0;}else{cout << "右左旋更新平衡因子出现问题" << endl;assert(nullptr);}}private:Node* _root;
};

2 红黑树

        对于红黑树相信大家在学之前就已经有所耳闻了,都知道他是一个很难的一个数据结构,但是不知道具体到底是个啥,那么今天就让博主为大家带来红黑树的写法。

2.1 红黑树概念

红黑树同样也是一种二叉搜索树,但是每一个结点会添加一个储存位表示结点的颜色,可以是黑色的结点,也可以是红色的结点,通过一定的限制手段,对任何一条从根到叶子结点的路径上各个结点着色方式的限制,确保没有任何一条路径比其它路径长出两倍。

每一个结点不是红色就是黑色

根节点的颜色是黑色

如果一个结点是红色的,那么它的两个孩子结点一定是黑色的

任何一条到叶子结点的路径上黑色结点的个数相等

空结点的颜色是黑色

         咱们来思考以下,为什么满足了这几点就能够满足最长路径的结点个数不会超过最短路径结点个数的两倍呢?

        首先通过解析第三条规则,翻译过来其实就是红色结点不能连续出现。因为黑色结点的个数在每一条路径上的个数必须保证相同,假设最短路径只有黑色的结点没有红色结点,最长路径因为红色结点不能连续出现,所以只能和黑色间隔插入,那么黑色结点的个数一定等于红色结点,那么假设黑色结点为N个,红色结点也最多只能是N个,所以最长的路径也不过是2N的长度罢了

2.2 红黑树与AVL树的比较

        可能有的小伙伴会有疑问,那就是为什么我们已经有了AVL树了,但是还需要一个红黑树呢?而且看起来红黑树并没有AVL树平衡啊?

        确实,大家有这样的疑问很正常,博主在学习时也有这样的疑问,但是很快就被打消了,为什么呢?AVL树确实比红黑树要平衡,但是为了这种平衡,他付出了很大的代价,那就是插入数据、删除数据这种操作会让他有大量的调整,而这个调整过程会有大量的时间消耗,对于红黑树来说,它却减少了这样的调整过程,毕竟它没有那么严格的调整要求。

        并且,虽然红黑树的最长路径可能是2N,AVL树的最长路径是N,但是别忘了我们的二叉树结构,因为二者都几乎是二分的方式查找,就算是一亿的长度,二者的查找效率也不过是log2_2N和log2_N的区别罢了,有很大的差距吗?没有的,相比于AVL树的调整消耗,这微不足道。

        所以工程上一半是使用红黑树作为底层数据结构而不是AVL树就是这个原因。AVL树只是一棵在理论上比红黑树更加优秀的树结构罢了。

2.3 红黑树结点结构

enum Color
{BLOCK = 0,RED = 1
};//结点为三叉链,数据,以及红黑表示,对于V这一部分,根据用户的使用而不同
template<class V>
struct RBTreeNode
{RBTreeNode<V>* _left;RBTreeNode<V>* _right;RBTreeNode<V>* _parent;Color _rb;V _data;//结点的有有参构造RBTreeNode(const V& data)//默认插入结点的颜色为红色:_left(nullptr), _right(nullptr), _parent(nullptr), _rb(RED), _data(data){}
};

        通过枚举结构作为红黑树结点内部的颜色表示,BLOCK表示黑色,RED表示红色。同样的,我们的结构通过三叉链实现,分别是左右结点指针,父节点指针,还有类型是V的数据。这个V类型在封装时有大用,大家可以带着它学习下去。

        构造将指针各个指针初始化为空,数据按照传参初始化,那么颜色为什么初始化为红色呢?

看下图:

         当我们插入12结点,并且它的颜色是红色时,这个树还是不是红黑树呢?是,如果插入在6、22、27下面就需要调整了,但是这也代表了用红色结点插入可能不会导致调整。再看插入黑色结点呢?

         插入了黑色的12结点会导致什么?各个路径上的黑色不能全部保证相同了,这还正确吗?不正确,所以需要调整,但是如何调整?要调整多少?整棵树,这可受不了,并且这种调整并不是说插入在红色结点下就能避免的,不信你们自己画图试试,所以,这表示如果插入的结点颜色是黑色则需要改动整颗树

        这样对比下来,还是插入一个红色的结点更好。

2.4 红黑树普通二叉搜索树插入

//插入
pair<iterator,bool> Insert(const V& data)
{//第一次插入if (_root == nullptr){//保证我们的根节点一定是黑色的,满足红黑树的条件_root = new Node(data);_root->_rb = BLOCK;NodeNum++;return make_pair(iterator(_root),true);}//后续的插入Node* cur = _root;Node* parent = nullptr;//找到插入位置while (cur){//这里的比较方式可以通过添加仿函数更改比较方式//小于if (_com(_kov(data), _kov(cur->_data))){parent = cur;cur = cur->_left;}//大于else if (_com(_kov(cur->_data), _kov(data))){parent = cur;cur = cur->_right;}//等于else{//已经有了重复的数据,那么这个位置是不需要再次插入return make_pair(iterator(cur), false);}}//已经找到了插入的位置,这个位置的parent一定不会为空,因为此时的cur为空cur = new Node(data);if (_com(_kov(data), _kov(parent->_data)))parent->_left = cur;elseparent->_right = cur;//节点个数调整以及结点的回连NodeNum++;cur->_parent = parent;
}

        大家暂时不要在意迭代器部分,之后我单独为大家讲解。可以看到,红黑树的插入部分与AVL树和搜索树没有任何的区别,唯一需要更改的就是第一次插入的时候需要将结点的颜色更改为黑色,只是博主还添加了一个记录有多少结点的变量罢了。没什么重要的和难度,博主相信大家看到这一段代码已经很轻松了。

2.5 红黑树的调整

2.5.1 当前结点为红、parent为红、grandfather为黑、uncle为红色

        也就是说,如果出现了叔叔、并且叔叔的颜色为红色,那么一定就只有一种调整方式,无论cur在p的左边还是右边,都是同样的调整方式。那就是将p和u的结点颜色变为黑色,g的颜色变为红色,其余结点的颜色不变化。如下:

         当我们变为了现在这样的这棵树代表了什么?那就是成功将当前树变为了红黑树,只是g的颜色应该是黑色的,现在是红色的罢了,那么这就代表了,如果g就是根节点,那么就将它的颜色变为黑色,如果它不是根,就不更改它的颜色

        可能有的小伙伴又有想法了,那就是如果g不是根结点,并且它的父亲结点还是红色怎么办?答案很简单,那就是将cur指向g,parent指向g的parent结点,也就是重复这个调整过程,为什么行呢?如果没有爷爷结点怎么办?我会回答不可能,如果g这个结点是红色的,而且它的父亲结点也是红色的,那么它一定有它的爷爷结点,因为我们规则中有一句话,那就是根节点必须是黑色的结点,所以一定能出现调整的情况。

        根据上面的几种情况,博主为大家画出了g这个结点的颜色和g的parent的颜色对应关系。

         当然在写完这些操作时候还要确定叔叔与父亲结点的位置关系,也就是谁是左谁是右,毕竟我们调整可不是单独判断一种情况。

代码:

//父亲结点在爷爷结点的左边
if (ppNode->_left == parent)
{Node* uncle = ppNode->_right;//调整1:cur为红色,parent为红,uncle为红,grandfather为红if (uncle && uncle->_rb == RED){//颜色调整parent->_rb = BLOCK;ppNode->_right->_rb = BLOCK;ppNode->_rb = RED;//如果爷爷结点为根节点,颜色变为黑色,并调整完毕if (ppNode == _root){ppNode->_rb = BLOCK;break;}//下一次调整parent = ppNode->_parent;cur = ppNode;}
}

2.5.2 cur为红、p为红、g为黑、叔叔不存在或则存在为黑色,cur、p、g在同一条线上

         当出现了上面的这两种情况就表示了需要通过第二种调整方式了,也就是右单旋加修改颜色的方式。

        首先我们分析第一种情况的出现原因,我们咋一看会认为第一种情况这棵树在之前就已经不是红黑树了,但是事实真的是这样吗?如果我们的cur是新插入的结点,那么就没有a、b结构,那么有叔叔的那一条路径就有了两个黑色结点,父亲这一条结点就只有一个黑色结点,这很明显就代表了原来的红黑树就已经错误了,所以在叔叔结点存在且为黑的时候,cur不可能是新插入的结点,它原来的颜色一定是黑色,但是被我们a、b结构插入结点时影响了

        那么如果叔叔结点不存在的时候呢,cur一定是新插入的结点,因为如果cur是被影响的那个结点,那么对于父亲这条路径至少有两个黑色结点,但是叔叔路径只有一个黑色结点,这很明显不是正确的红黑树结构,所以,在叔叔不存在的时候,cur一定只能是新插入的结点

        但是对于我们来说,这两种情况是可以归为一类的,因为叔叔的存在与否,和叔叔的颜色只是为了帮助我们去判断这种情况需要通过第二种调整方式而已。

        首先,当我们检测到了cur和p的颜色是红色,cur在p的左边,g是黑色,p在g的左边,叔叔存在为黑,或则它不存在的情况时,就需要先右单旋爷爷结点的位置。然后再把爷爷节点改为红色,父亲结点改为黑色。如下图:

         那么为什么能这么更改呢?首先我们知道,当出现了p为红,cur为红,g为黑,u为黑的情况只能将p改为黑色,g改为红色结点,那么当前位置的结构才不会出问题,但是父亲位置路径会多出一个黑色结点,这个时候就需要把父亲结点向上升级,将他变为爷爷位置,爷爷位置下来,因为爷爷已经被变为了红色结点,所以移动下来不改变黑色结点的个数,也不会出现两个红色冲突的情况,这个时候将父亲作为两个路径的共同黑色结点,就不会让它们任何一条路径多一个黑色结点,并且由于这两个路径的黑色结点个数没有任何的变化,所以变化完成之后,不会影响上级的路径,可以在这个地方结束调整了,也就是break。

代码:

//叔叔结点不存在,或则是叔叔结点为黑色的情况
else
{//cur结点在parent的左边if (parent->_left == cur){//右旋RotateR(ppNode);parent->_rb = BLOCK;ppNode->_rb = RED;}
}

        旋转代码博主就不在这里为大家附上了,旋转的代码就是AVL树的旋转,直接把上面的拿下来就行,只需要删除里面对于平衡因子的调整 部分就行颜色部分被我放到了上面这一段代码当中。

2.5.3 cur为红,p为红,g为黑,u不存在或则u存在为黑,cur与p和g是折线关系

         如图情况就是我们的第三种调整方式,看到这个图,我相信大家也能想起来之前AVL树的双旋问题,需要把cur、p、g放到一条直线上来,然后再是单旋操作。这一步我相信大家也很熟悉了。

        这种情况的出现条件我也不再赘述了,和调整二的出现原因一致。

        对于调整三来说,第一次调整只是为了完成cur与p的位置关系满足调整二,整体没有任何的颜色需要变化,通过旋转调整位置关系之后,然后再旋转满足红黑树条件。 

代码:

//cur结点在parent的右边
else
{RotateL(parent);RotateR(ppNode);cur->_rb = BLOCK;ppNode->_rb = RED;
}

 2.6 红黑树插入代码

        红黑树博主也只是实现了它的插入部分,因为重点不是它的复现,而是结构的理解。而且博主并没有为大家讲解cur在g的右面的插入情况,但是博主相信大家通过代码也是能够明白的,毕竟与博主解释的那一部分代码正好逻辑相反,也没什么好解释的。

#pragma once
#include<iostream>
using namespace std;enum Color
{BLOCK = 0,RED = 1
};//默认比较方式
template<class K>
class Less
{
public:bool operator()(const K& L1, const K& L2){return L1 < L2;}
};//结点为三叉链,数据,以及红黑表示,对于V这一部分,根据用户的使用而不同
template<class V>
struct RBTreeNode
{RBTreeNode<V>* _left;RBTreeNode<V>* _right;RBTreeNode<V>* _parent;Color _rb;V _data;//结点的有有参构造RBTreeNode(const V& data)//默认插入结点的颜色为红色:_left(nullptr), _right(nullptr), _parent(nullptr), _rb(RED), _data(data){}
};//V有可能是pair也有可能是key
template<class K, class V, class KeyOfValue, class Compare>
class RBTree
{typedef RBTreeNode<V> Node;
public:typedef __RBTreeIterator<V, V&, V*> iterator;typedef __RBTreeIterator<V, const V&, const V*> const_iterator;
public://普通迭代器iterator begin(){return iterator(leftmost());}iterator end(){return iterator(nullptr);}//const迭代器const_iterator begin() const{//return const_iterator(leftmost());return leftmost();}const_iterator end() const{//return const_iterator(nullptr);return nullptr;}//最左值Node* leftmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_left){cur = cur->_left;}return cur;}//最右值Node* rightmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_right){cur = cur->_right;}return cur;}//如果找到或则没有找到都返回对应结点的迭代器,让用户自己操作iterator find(const V& data) const{Node* cur = _root;while (cur){if (_com(_kov(data), _kov(cur->_data))){cur = cur->_left;}else if (_kov(cur->_data), _com(_kov(data))){cur = cur->_right;}else{return iterator(cur);}}return iterator(nullptr);}//插入pair<iterator,bool> Insert(const V& data){//第一次插入if (_root == nullptr){//保证我们的根节点一定是黑色的,满足红黑树的条件_root = new Node(data);_root->_rb = BLOCK;NodeNum++;return make_pair(iterator(_root),true);}//后续的插入Node* cur = _root;Node* parent = nullptr;//找到插入位置while (cur){//这里的比较方式可以通过添加仿函数更改比较方式//小于if (_com(_kov(data), _kov(cur->_data))){parent = cur;cur = cur->_left;}//大于else if (_com(_kov(cur->_data), _kov(data))){parent = cur;cur = cur->_right;}//等于else{//已经有了重复的数据,那么这个位置是不需要再次插入return make_pair(iterator(cur), false);}}//已经找到了插入的位置,这个位置的parent一定不会为空,因为此时的cur为空cur = new Node(data);if (_com(_kov(data), _kov(parent->_data)))parent->_left = cur;elseparent->_right = cur;//节点个数调整以及结点的回连NodeNum++;cur->_parent = parent;//红黑树调整部分Node* Insert_Pos = cur;//进入循环表示爷爷结点存在,因为parent结点是红色while (parent && parent->_rb == RED){//爷爷结点Node* ppNode = parent->_parent;//父亲结点在爷爷结点的左边if (ppNode->_left == parent){Node* uncle = ppNode->_right;//调整1:cur为红色,parent为红,uncle为红,grandfather为红if (uncle && uncle->_rb == RED){//颜色调整parent->_rb = BLOCK;ppNode->_right->_rb = BLOCK;ppNode->_rb = RED;//如果爷爷结点为根节点,颜色变为黑色,并调整完毕if (ppNode == _root){ppNode->_rb = BLOCK;break;}//下一次调整parent = ppNode->_parent;cur = ppNode;}//叔叔结点不存在,或则是叔叔结点为黑色的情况else{//cur结点在parent的左边if (parent->_left == cur){//右旋RotateR(ppNode);parent->_rb = BLOCK;ppNode->_rb = RED;}//cur结点在parent的右边else{RotateL(parent);RotateR(ppNode);cur->_rb = BLOCK;ppNode->_rb = RED;}break;}}//当父亲结点在爷爷的右边else{Node* uncle = ppNode->_left;if (uncle && uncle->_rb == RED){//颜色调整parent->_rb = BLOCK;ppNode->_left->_rb = BLOCK;ppNode->_rb = RED;//如果爷爷结点为根节点,颜色变为黑色,并调整完毕if (ppNode == _root){ppNode->_rb = BLOCK;break;}//下一次调整parent = ppNode->_parent;cur = ppNode;}//叔叔结点为黑色或则是不存在else{if (parent->_right == cur){//左旋RotateL(ppNode);parent->_rb = BLOCK;ppNode->_rb = RED;}else{RotateR(parent);RotateL(ppNode);cur->_rb = BLOCK;ppNode->_rb = RED;}}}}return make_pair(iterator(Insert_Pos), true);}void  _Inorder(Node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_data << " ";_Inorder(root->_right);}void Inorder(){_Inorder(_root);}bool isConform(){Node* cur = _root;int PathSize = 1;//随便找一条最短路径while (cur){if(cur->_rb == BLOCK) ++PathSize;cur = cur->_left;}return _isConform(_root,PathSize,1);}
protected://判断是否符合红黑树规则bool _isConform(Node* root, const int PathSize, int Num){//空红黑或则是空结点if (root == nullptr){//判断黑色结点的个数if (PathSize == Num)return true;return false;}//判断红结点if (root->_rb == RED && root->_parent){//出现连续的红结点if (root->_parent->_rb == RED)return false;}//递归return _isConform(root->_left, PathSize, Num + 1) &&_isConform(root->_right, PathSize, Num + 1);}//右旋void RotateR(Node* parent){//结点表示Node* SubL = parent->_left;Node* SubLR = SubL->_right;//SubL右节点断链节与parent结点的左连接parent->_left = SubLR;if (SubLR != nullptr){SubLR->_parent = parent;}//爷爷结点Node* ppNode = parent->_parent;SubL->_right = parent;parent->_parent = SubL;//父节点为根if (parent == _root){_root = SubL;SubL->_parent = nullptr;}//不是则需要连接else{if (ppNode->_left == parent){ppNode->_left = SubL;}else{ppNode->_right = SubL;}SubL->_parent = ppNode;}}//左旋void RotateL(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* ppNode = parent->_parent;parent->_right = SubRL;if (SubRL != nullptr){SubRL->_parent = parent;}SubR->_left = parent;parent->_parent = SubR;if (parent == _root){_root = SubR;SubR->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = SubR;}else{ppNode->_right = SubR;}SubR->_parent = ppNode;}}private://红黑树私有变量Node* _root;int NodeNum = 0;//红黑树仿函数对象,比较方式和key值访问方式Compare _com;KeyOfValue _kov;
};

3 map和set的封装

3.1 模板参数介绍

        终于到这里了,博主也得感叹不容易啊,看到这里的小伙伴值得点赞。

        map和set博主这里模仿了库的实现方式,通过模板同时将一个红黑树作为两个容器的底层。

        首先咱们看到这一段代码:

template<class K, class V, class KeyOfValue, class Compare>
class RBTree
{
}

         这是一个模板的写法,没问题,但是分别对应什么大家知道吗?K还是和我们相信的一样,是一个key比较的数据,但是这个V呢?是value值?不不不,很明显没有这么简单的,博主说了,我们的模板是要用于两个容器用的,那么这个V的类型就有可能是K,也可能是pair<K,V>这表示了什么?这表示当我们传入的是一个K那么它就是一个set,如果传入一个pair,他就是map了,有没有感觉很牛逼?更牛逼的还在后面呢,毕竟直接这样写会有一大推的问题。

        那么首先面临的第一个问题是什么?那就是红黑树里面对于K的访问怎么做?如果是set我们确实可以不用改变写法,直接对key进行比较就行,因为这就是我们想要的答案,但是map呢?它可是一个pair啊,它的直接比较是我们希望的那种比较方式吗?肯定不是哇,它可不敢为我们做这种事情,所以比较出来一定是错的,这个时候,第三个模板参数KeyOfValue就派上用场了,我们通过这个模板参数传入了一个仿函数,也就是不同的V有不同的比较方式,什么意思呢?如下代码:

//set的仿函数
class KeyOfValue
{
public:const K& operator()(const K& key){return key;}
};//map的仿函数
class KeyOfMap
{
public:const K operator()(const pair<const K, V>& L){return L.first;}
};

        通过仿函数,相信大家也能够反应过来,只要我们通过仿函数获取key值,无论我们的V是K还是pair都对我们没有任何的影响,因为这两个仿函数返回的值都是key值,也就是我们希望比较的方式。

        那么第四个参数compare是用来干嘛的呢?这个参数其实是博主的额外兴趣,这个参数也是一个仿函数,只不过这个仿函数是用来控制比较的方式的,例如,如果我希望实现一个中序访问,实现降序的树结构,通过修改这个仿函数的比较方式就能得到。把选择权交给了用户,当然,这一部分是没有必要的。

//默认比较方式
template<class K>
class Less
{
public:bool operator()(const K& L1, const K& L2){return L1 < L2;}
};

        那么根据这样的方式,我们的代码所有的key比较部分就需要修改了,不过博主之前已经更改完成了,所以就为大家再显示部分。

if (_com(_kov(data), _kov(cur->_data))

//红黑树仿函数对象,比较方式和key值访问方式
Compare _com;
KeyOfValue _kov;

        可能有一部分朋友会认为这部分有一些难以理解,那博主就再讲一下,那就是我们的红黑树虽然不知道我们到底是map还是set,但是对于map和set来说,它们自己肯定是知道的,而红黑树对象就在它们自己的类当中,只要自己传模板类型就好,所以才让红黑树知道自己到底是谁的。

        也就是如下代码,分别在map和set当中的声明:

RBTree<K, pair<K, V>, KeyOfMap<const K, V>, Less<const K>> _t;

RBTree<K, K, KeyOfSet<K>, Less<K>> _t;

        这样的传参方式,红黑树还知道自己的类型吗?肯定是知道的哇,也就是不同的红黑树用同样的结构写出来了,没什么很难理解的。

3.2 迭代器的实现

        要说容器哪里厉害,那还得是迭代器,但是这里map和set的迭代器都是套壳所以,博主这里就为大家讲解红黑树的迭代器是如何实现的。

template<class T, class Ref, class Ptr>
class __RBTreeIterator
{
public:typedef RBTreeNode<T> Node;Node* _node;
};

           首先看到迭代器的结构,和我们的list容器的迭代器是相同的实现方式,也是另外创建了一个迭代器类,这个类当中的变量是某个结点的指针,通过交换指针的方式,得到不同的迭代器,进而可以与红黑树配合起来。

        首先看到它的模板参数,分别是T,Ref,Ptr,也就是T、T&、T*这三个东西,当然这没什么,无论T是什么对于这个迭代器来说都是一样的,因为它只需要某个结点的指针罢了,需要T的地方是结点里面的数据类型需要。

        首先看到迭代器的基本功能:

//结构体
Ref operator*()
{return _node->_data;
}//结构体地址
Ptr operator->()
{return &_node->_data;
}//结点地址不同则不同
bool operator!=(const Self& s)
{return _node != s._node;
}

         以上实现的就是*iterator、iterator->的支持方式,!=的实现是为了范围for和迭代器遍历准备的,那么,我们的红黑树应该怎么遍历起来呢?

3.2.1 迭代器++操作

        我们得知道,我们希望实现迭代器得访问出现的数据也是中序访问,也就是有序得访问,那么表示了什么?那就是先访问左节点,然后根节点,最后就是右结点。

        也就是,当我们走到了某一个结点位置,那么必定表示这个结点是上一次访问的,也就代表了我们下一次走的位置不是右子树的最左结点,就是向上查找子树不是父节点的右结点的那个父节点。

        根据这个逻辑,我们就可以判断当前结点的右子树是空还是有结点,如果有结点表示就需要走到下一个子树当中,那么就通过循环往下找到子树的最左结点,如果右子树没有结点了,那么就表示了这个树结构已经走完了,那么就需要向上走,找父节点,这个结点的上级不一定就是我们希望访问的那个结点,需要找到父节点的左子树是当前结点,而不是右子树,因为右子树相同表示我们已经访问过了,只有左子树才是没有访问过的那个

Self& operator++()
{//右孩子不为空,下一次访问的是右边最左结点if (_node->_right != nullptr){Node* nextNode = _node->_right;while (nextNode->_left){nextNode = nextNode->_left;}_node = nextNode;}//右孩子为空,下一次往上查找孩子不是父亲右结点的结点else{Node* cur = _node;Node* nextNode = _node->_parent;while (nextNode && nextNode->_right == cur){cur = nextNode;nextNode = nextNode->_parent;}_node = nextNode;}return *this;
}

3.2.2 迭代器--操作

        --操作,博主不想过多的讲解,因为我们只需要反过来看中序访问的顺序,也就是右子树,根节点,左子树的访问方式就能实现了。

//减与加正好逻辑相反
Self& operator--()
{if (_node->_left != nullptr){Node* prevNode = _node->_left;while (prevNode->_right){prevNode = prevNode->_right;}_node = prevNode;}else{Node* cur = _node;Node* prevNode = _node->_parent;while (prevNode && prevNode->_left){cur = prevNode;prevNode = prevNode->_parent;}_node = prevNode;}return *this;
}

3.2.3 红黑树内迭代器访问操作

template<class K, class V, class KeyOfValue, class Compare>
class RBTree
{typedef RBTreeNode<V> Node;
public:typedef __RBTreeIterator<V, V&, V*> iterator;typedef __RBTreeIterator<V, const V&, const V*> const_iterator;
public://普通迭代器iterator begin(){return iterator(leftmost());}iterator end(){return iterator(nullptr);}//const迭代器const_iterator begin() const{//return const_iterator(leftmost());return leftmost();}const_iterator end() const{//return const_iterator(nullptr);return nullptr;}//最左值Node* leftmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_left){cur = cur->_left;}return cur;}//最右值Node* rightmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_right){cur = cur->_right;}return cur;}
}

3.3 map的封装

namespace YF
{//获取pair当中的K值template<class K,class V>class KeyOfMap{public:const K operator()(const pair<const K, V>& L){return L.first;}};template<class K, class V, class Compare = Less<K>>class MyMap{public:typedef typename RBTree<K, pair<K,V>, KeyOfMap<K,V>, Less<K>>::iterator iterator;typedef typename RBTree<K, pair<K, V>, KeyOfMap<K, V>, Less<K>>::const_iterator const_iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}const_iterator begin() const {return _t.begin();}const_iterator end() const{return _t.end();}V& operator[](const K& key){pair<iterator, bool> res = _t.Insert(make_pair(key, V()));return res.first->second;}pair<iterator, bool> insert(const pair<K, V>& data){return _t.Insert(data); }private:RBTree<K, pair<K, V>, KeyOfMap<K, V>, Less<K>> _t;};
}

         对于map和set来说,我们的insert操作会在插入失败的时候返回这个结点的迭代器,如果插入成功之后,就会返回插入结点的迭代器。通过bool类型告诉用户档次插入是否成功。

        如下就是两种不同的返回方式。

return make_pair(iterator(_root),true);

return make_pair(iterator(cur), false);

         对于map来说还希望实现一个[ ]查询的功能,那么需要运算符重载[ ],因为它需要满足一个功能那就是没有当前的key,自动为我插入,所以这就表示了,我们需要在这个函数调用insert函数,因为insert在找到时返回找到的结点的迭代器,没找到返回新插入结点的迭代器。所以根据这种方式,我们就能获取相应的[ ]功能了。

3.4 set的封装

namespace YF
{template<class K>class KeyOfSet{public:K operator()(const K& key){return key;}};template<class K, class Compare = Less<K>>class MySet{public:typedef typename RBTree<K,K, KeyOfSet<K>,Less<K>>::iterator iterator;typedef typename RBTree<K, K, KeyOfSet<K>, Less<K>>::const_iterator const_iterator;//为了满足map与set的使用方式的区别,它们两个的寻值都是通过仿函数进行的class KeyOfValue{public:const K& operator()(const K& key){return key;}};pair<iterator,bool> insert(const K& key){return _t.Insert(key);}iterator begin(){return _t.begin();}iterator end(){return _t.end();}void Inorder(){_t.Inorder();}private:RBTree<K, K, KeyOfSet<K>, Less<K>> _t;};
}

        现在实现的map和set的封装没有什么太区别,所以我也不多作讲解,重要的时下方的优化部分。

3.5 map和set封装优化

3.5.1 set的优化

        根据我上面的map和set的封装确实能够实现它们各自对应的功能,但是呢,对应的key值能够被更改,这是不允许的,因为这会破坏红黑树的结构,所以我模仿stl库做出了以下的更改。

set当中:

typedef typename RBTree<K,K, KeyOfSet<K>,Less<K>>::const_iterator iterator;
typedef typename RBTree<K, K, KeyOfSet<K>, Less<K>>::const_iterator const_iterator;

         它的普通迭代器和const迭代器都使用了红黑树的const迭代器,也就表示传入的参数实际上就是一个const K,那么返回给我们的结构就不会允许我们修改key值。

        但是这么做有问题,也就是会出现如下报错:

“return”: 无法从“std::pair<__RBTreeIterator<V,V &,V *>,bool>”转换为“std::pair<__RBTreeIterator<V,const V &,const V *>,bool>”

         错误原因就是这个return 无法返回,因为我们的代码有单参数的转换,但是却没有普通迭代器到const迭代器的转换,也就是说在红黑树当中的iterator迭代器就是普通迭代器,但是我们的set里面的iterator却是const_iterator,所以说无法支持转换,那么解决方案是什么呢?那就是让他支持这样的转换方式。所就是写一个迭代器的转换。

        也就是如下代码:

    typedef __RBTreeIterator<T, T&, T*> iterator;
    typedef __RBTreeIterator<T, const T&,const T*> const_iterator;

__RBTreeIterator(const iterator& it)
        :_node(it._node) {}

         我们的迭代器无论时const还是普通的,我们在迭代器类当中都是用普通迭代器构造,也就是如果以后我们之后在转换的时候,如果是普通迭代器,那么他就是拷贝构造,如果是const迭代器,那么他就是一个支持普通迭代器构造const迭代器的构造函数。也就支持了我们的转换。

         此时再看就不会出错了。

 3.5.2 map的优化

typedef typename RBTree<K, pair<const K,V>, KeyOfMap<K,V>, Less<K>>::iterator iterator;typedef typename RBTree<K, pair<const K, V>, KeyOfMap<K, V>, Less<K>>::const_iterator const_iterator;pair<iterator, bool> insert(const pair<const K, V>& data)
{return _t.Insert(data); 
}RBTree<K, pair<const K, V>, KeyOfMap<const K, V>, Less<const K>> _t;

        map的修改就比较简单了,它的普通迭代器就是普通迭代器,const迭代器还是const迭代器,但是只要我们在传入参数的时候,就把所有的K加上const传入,就能表示map的key不能被修改。

4 map和set的完整实现代码

4.1 map

#pragma once#include"RBTree.h"namespace YF
{//获取pair当中的K值template<class K,class V>class KeyOfMap{public:const K operator()(const pair<const K, V>& L){return L.first;}};template<class K, class V, class Compare = Less<K>>class MyMap{public:typedef typename RBTree<K, pair<const K,V>, KeyOfMap<K,V>, Less<K>>::iterator iterator;typedef typename RBTree<K, pair<const K, V>, KeyOfMap<K, V>, Less<K>>::const_iterator const_iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}const_iterator begin() const {return _t.begin();}const_iterator end() const{return _t.end();}V& operator[](const K& key){pair<iterator, bool> res = _t.Insert(make_pair(key, V()));return res.first->second;}pair<iterator, bool> insert(const pair<const K, V>& data){return _t.Insert(data); }private:RBTree<K, pair<const K, V>, KeyOfMap<const K, V>, Less<const K>> _t;};
}

4.2 set代码

#include"RBTree.h"namespace YF
{template<class K>class KeyOfSet{public:K operator()(const K& key){return key;}};template<class K, class Compare = Less<K>>class MySet{public:typedef typename RBTree<K,K, KeyOfSet<K>,Less<K>>::const_iterator iterator;typedef typename RBTree<K, K, KeyOfSet<K>, Less<K>>::const_iterator const_iterator;//为了满足map与set的使用方式的区别,它们两个的寻值都是通过仿函数进行的class KeyOfValue{public:const K& operator()(const K& key){return key;}};pair<iterator,bool> insert(const K& key){return _t.Insert(key);}iterator begin() const {return _t.begin();}iterator end() const {return _t.end();}void Inorder(){_t.Inorder();}private:RBTree<K, K, KeyOfSet<K>, Less<K>> _t;};
}

4.3 红黑树代码

#pragma once
#include<iostream>
using namespace std;enum Color
{BLOCK = 0,RED = 1
};//默认比较方式
template<class K>
class Less
{
public:bool operator()(const K& L1, const K& L2){return L1 < L2;}
};//结点为三叉链,数据,以及红黑表示,对于V这一部分,根据用户的使用而不同
template<class V>
struct RBTreeNode
{RBTreeNode<V>* _left;RBTreeNode<V>* _right;RBTreeNode<V>* _parent;Color _rb;V _data;//结点的有有参构造RBTreeNode(const V& data)//默认插入结点的颜色为红色:_left(nullptr), _right(nullptr), _parent(nullptr), _rb(RED), _data(data){}
};template<class T, class Ref, class Ptr>
class __RBTreeIterator
{
public:typedef RBTreeNode<T> Node;typedef __RBTreeIterator<T, Ref, Ptr> Self;typedef __RBTreeIterator<T, T&, T*> iterator;typedef __RBTreeIterator<T, const T&,const T*> const_iterator;__RBTreeIterator(Node* node):_node(node){}__RBTreeIterator(const iterator& 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 != nullptr){Node* nextNode = _node->_right;while (nextNode->_left){nextNode = nextNode->_left;}_node = nextNode;}//右孩子为空,下一次往上查找孩子不是父亲右结点的结点else{Node* cur = _node;Node* nextNode = _node->_parent;while (nextNode && nextNode->_right == cur){cur = nextNode;nextNode = nextNode->_parent;}_node = nextNode;}return *this;}//减与加正好逻辑相反Self& operator--(){if (_node->_left != nullptr){Node* prevNode = _node->_left;while (prevNode->_right){prevNode = prevNode->_right;}_node = prevNode;}else{Node* cur = _node;Node* prevNode = _node->_parent;while (prevNode && prevNode->_left){cur = prevNode;prevNode = prevNode->_parent;}_node = prevNode;}return *this;}Node* _node;
};//V有可能是pair也有可能是key
template<class K, class V, class KeyOfValue, class Compare>
class RBTree
{typedef RBTreeNode<V> Node;
public:typedef __RBTreeIterator<V, V&, V*> iterator;typedef __RBTreeIterator<V, const V&, const V*> const_iterator;
public://普通迭代器iterator begin(){return iterator(leftmost());}iterator end(){return iterator(nullptr);}//const迭代器const_iterator begin() const{//return const_iterator(leftmost());return leftmost();}const_iterator end() const{//return const_iterator(nullptr);return nullptr;}//最左值Node* leftmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_left){cur = cur->_left;}return cur;}//最右值Node* rightmost() const {if (_root == nullptr)return nullptr;Node* cur = _root;while (cur->_right){cur = cur->_right;}return cur;}//如果找到或则没有找到都返回对应结点的迭代器,让用户自己操作iterator find(const V& data) const{Node* cur = _root;while (cur){if (_com(_kov(data), _kov(cur->_data))){cur = cur->_left;}else if (_kov(cur->_data), _com(_kov(data))){cur = cur->_right;}else{return iterator(cur);}}return iterator(nullptr);}//插入pair<iterator,bool> Insert(const V& data){//第一次插入if (_root == nullptr){//保证我们的根节点一定是黑色的,满足红黑树的条件_root = new Node(data);_root->_rb = BLOCK;NodeNum++;return make_pair(iterator(_root),true);}//后续的插入Node* cur = _root;Node* parent = nullptr;//找到插入位置while (cur){//这里的比较方式可以通过添加仿函数更改比较方式//小于if (_com(_kov(data), _kov(cur->_data))){parent = cur;cur = cur->_left;}//大于else if (_com(_kov(cur->_data), _kov(data))){parent = cur;cur = cur->_right;}//等于else{//已经有了重复的数据,那么这个位置是不需要再次插入return make_pair(iterator(cur), false);}}//已经找到了插入的位置,这个位置的parent一定不会为空,因为此时的cur为空cur = new Node(data);if (_com(_kov(data), _kov(parent->_data)))parent->_left = cur;elseparent->_right = cur;//节点个数调整以及结点的回连NodeNum++;cur->_parent = parent;//红黑树调整部分Node* Insert_Pos = cur;//进入循环表示爷爷结点存在,因为parent结点是红色while (parent && parent->_rb == RED){//爷爷结点Node* ppNode = parent->_parent;//父亲结点在爷爷结点的左边if (ppNode->_left == parent){Node* uncle = ppNode->_right;//调整1:cur为红色,parent为红,uncle为红,grandfather为红if (uncle && uncle->_rb == RED){//颜色调整parent->_rb = BLOCK;ppNode->_right->_rb = BLOCK;ppNode->_rb = RED;//如果爷爷结点为根节点,颜色变为黑色,并调整完毕if (ppNode == _root){ppNode->_rb = BLOCK;break;}//下一次调整parent = ppNode->_parent;cur = ppNode;}//叔叔结点不存在,或则是叔叔结点为黑色的情况else{//cur结点在parent的左边if (parent->_left == cur){//右旋RotateR(ppNode);parent->_rb = BLOCK;ppNode->_rb = RED;}//cur结点在parent的右边else{RotateL(parent);RotateR(ppNode);cur->_rb = BLOCK;ppNode->_rb = RED;}break;}}//当父亲结点在爷爷的右边else{Node* uncle = ppNode->_left;if (uncle && uncle->_rb == RED){//颜色调整parent->_rb = BLOCK;ppNode->_left->_rb = BLOCK;ppNode->_rb = RED;//如果爷爷结点为根节点,颜色变为黑色,并调整完毕if (ppNode == _root){ppNode->_rb = BLOCK;break;}//下一次调整parent = ppNode->_parent;cur = ppNode;}//叔叔结点为黑色或则是不存在else{if (parent->_right == cur){//左旋RotateL(ppNode);parent->_rb = BLOCK;ppNode->_rb = RED;}else{RotateR(parent);RotateL(ppNode);cur->_rb = BLOCK;ppNode->_rb = RED;}}}}return make_pair(iterator(Insert_Pos), true);}void  _Inorder(Node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_data << " ";_Inorder(root->_right);}void Inorder(){_Inorder(_root);}bool isConform(){Node* cur = _root;int PathSize = 1;//随便找一条最短路径while (cur){if(cur->_rb == BLOCK) ++PathSize;cur = cur->_left;}return _isConform(_root,PathSize,1);}
protected://判断是否符合红黑树规则bool _isConform(Node* root, const int PathSize, int Num){//空红黑或则是空结点if (root == nullptr){//判断黑色结点的个数if (PathSize == Num)return true;return false;}//判断红结点if (root->_rb == RED && root->_parent){//出现连续的红结点if (root->_parent->_rb == RED)return false;}//递归return _isConform(root->_left, PathSize, Num + 1) &&_isConform(root->_right, PathSize, Num + 1);}//右旋void RotateR(Node* parent){//结点表示Node* SubL = parent->_left;Node* SubLR = SubL->_right;//SubL右节点断链节与parent结点的左连接parent->_left = SubLR;if (SubLR != nullptr){SubLR->_parent = parent;}//爷爷结点Node* ppNode = parent->_parent;SubL->_right = parent;parent->_parent = SubL;//父节点为根if (parent == _root){_root = SubL;SubL->_parent = nullptr;}//不是则需要连接else{if (ppNode->_left == parent){ppNode->_left = SubL;}else{ppNode->_right = SubL;}SubL->_parent = ppNode;}}//左旋void RotateL(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* ppNode = parent->_parent;parent->_right = SubRL;if (SubRL != nullptr){SubRL->_parent = parent;}SubR->_left = parent;parent->_parent = SubR;if (parent == _root){_root = SubR;SubR->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = SubR;}else{ppNode->_right = SubR;}SubR->_parent = ppNode;}}private://红黑树私有变量Node* _root;int NodeNum = 0;//红黑树仿函数对象,比较方式和key值访问方式Compare _com;KeyOfValue _kov;
};

        以上就是博主对于红黑树封装set和map的全部理解了,希望对大家有帮助。


http://www.ppmy.cn/news/71342.html

相关文章

轻松实现一个Python+Selenium的自动化测试框架

首先你得知道什么是Selenium&#xff1f; Selenium是一个基于浏览器的自动化测试工具&#xff0c;它提供了一种跨平台、跨浏览器的端到端的web自动化解决方案。Selenium主要包括三部分&#xff1a;Selenium IDE、Selenium WebDriver 和Selenium Grid。 Selenium IDE&#xff1…

Unity VR开发教程 OpenXR+XR Interaction Toolkit 番外(一)用 Grip 键, Trigger 键和摇杆控制手部动画

文章目录 &#x1f4d5;制作手部动画&#x1f4d5;设置 Animation Controller&#x1f4d5;添加触摸摇杆的 Input Action&#x1f4d5;代码部分 在大部分 VR 游戏中&#xff0c;手部的动画通常是由手柄的三个按键来控制的。比如 Grip 键控制中指、无名指、小拇指的弯曲&#xf…

【JS】1686- 重学 JavaScript API - Clipboard API

&#x1f3dd; 1. 什么是 Clipboard API 1.1 概念介绍 Clipboard API[1] 是一组 JavaScript API&#xff0c;用于在浏览器中操作剪贴板。通过 Clipboard API&#xff0c;开发者可以将文本、图片和其他数据复制到剪贴板&#xff0c;也可以从剪贴板中读取数据&#xff0c;实现复制…

Python语言基本控制结构

Python语言基本控制结构包括&#xff1a;条件语句&#xff1a;if、elif、else 循环语句&#xff1a;for、while 跳转语句&#xff1a;break、continue、return 下面是它们的基本用法&#xff1a; 条件语句 if condition1: statement1 elif condition2: statement2 else: stat…

【2023秋招】每日一题:P1087-美团3-18真题 + 题目思路 + 所有语言带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

Elasticsearch:了解和使用 match 查询

Match query 是针对多个用例的最常见和最强大的查询。 它是一个全文搜索查询&#xff0c;返回符合指定条件的文档。 match query 可以即兴使用来查询多个选项。在我之前的文章 “开始使用 Elasticsearch &#xff08;2&#xff09;” 对它有很多的描述。 Match 查询的格式 让我…

C++基础篇:01 简介

1 历史由来 本贾尼.斯特劳斯特卢普&#xff0c;于1979年在贝尔实验室分析UNIX系统分布式内核的流量时&#xff0c;特别希望有一种更加模块化的工具&#xff0c;于是在1979年10月时开始着手开发一块新的编程语言&#xff0c;在c语言的基础上增加了面向对象的机制&#xff0c;这就…

韩国访问学者签证D-2-5材料准备及签证流程

韩国的签证种类很多&#xff0c;对于申请访问学者签证来说&#xff0c;较常见的签证种类是D-2-5签证和E-3签证&#xff0c;本篇知识人网小编先介绍D-2-5签证。 签证的材料准备 根据韩国大使馆2023年4月12日最新发布的“签证申请与准备材料指导”内容, D-2-5签证的签发对象及准…

Mongodb Shell 常用操作命令

目录 一、启动与关闭mongodb服务 二、进入shell操作 三、常用shell命令 一、启动与关闭mongodb服务 启动:命令: ./mongod -config ../data/mongodb.conf 关闭命令: ./mongod -config ../data/mongodb.conf -shutdown 二、进入shell操作 命令:./mongo 三、常用shell命令 sh…

ES6中Iterator和for...of

1.Iterator 的遍历过程 说明&#xff1a; 遍历对象本质上就是一个指针对象。第一次调用指针对象的next方法&#xff0c;可以将指针指向数据结构的第一个成员。第二次调用指针对象的next方法&#xff0c;可以将指针指向第二个成员。依次进行。 2.默认Iterator接口 说明&…

【JavaEE】wait/notify方法 和 单例模型

目录 前言 1、 wait和notify 1.1、wait()方法 1.2、notify&#xff08;&#xff09;方法 1.3、wait和sleep 的对比 2、单例模式 2.1、饿汉模式 2.2、懒汉模式 2.3、上述懒汉模式和饿汉模式在多线程情况下是否安全 2.3.1、解决懒汉模式多线程不安去问题 前言 这里补充…

AutoSizer.exe:自动调整窗口大小的便捷工具

AutoSizer.exe是一款实用的桌面应用程序,它旨在帮助用户自动调整窗口大小,提供更好的用户体验。无论您是在使用Windows操作系统进行日常工作还是进行多任务处理,AutoSizer.exe可以简化您的工作流程,提高效率。本文将介绍AutoSizer.exe的下载地址、功能介绍、使用方法以及其…

ROS:launch文件加载:已有地图(yaml)、rviz、turtlebot3模型、gazebo模型、move_base、amcl

一.下载turtlebot3、建立地图文件yaml和pgm ROS&#xff1a;gazebo创建仿真地图&#xff0c;turtlebot3加载仿真地图进行建图&#xff0c;生成yaml和pgm地图信息_Charlesffff的博客-CSDN博客 二.创建目录 其中amcl.launch和move_base.launch目录在turtlebot3中&#xff1a; 其…

从 Elasticsearch 到 Apache Doris,10 倍性价比的新一代日志存储分析平台

日志数据的处理与分析是最典型的大数据分析场景之一&#xff0c;过去业内以 Elasticsearch 和 Grafana Loki 为代表的两类架构难以同时兼顾高吞吐实时写入、低成本海量存储、实时文本检索的需求。Apache Doris 借鉴了信息检索的核心技术&#xff0c;在存储引擎上实现了面向 AP …

Redis---主从复制 哨兵

目录 一、主从复制 1、什么是主从复制呢&#xff1f; 2、案例演示 2.1 配置文件 2.2 一主二仆 2.2.1 相关题目&#xff1a; 2.3 薪火相传 & 反客为主 3、复制原理和工作流程 3.1、slave启动&#xff0c;同步清初 3.2 首次连接&#xff0c;全量复制 3.…

Keil5----Debug时,watch1中全局变量数值不刷新问题解决方法

问题&#xff1a; 在Keil5-MDK中&#xff0c;Debug时&#xff0c;watch1中全局变量数值不刷新。 解决方法&#xff1a; 步骤1&#xff1a;进入Debug模式 将程序调试下载器&#xff08;STlink,Jlink,Ulink&#xff09;连接&#xff0c;编译程序后。 进行如下操作&#xff1a…

一个非常sb的报错……idea创建项目初始化失败……

今天在用idea创建项目时报错项目初始化失败&#xff1b; 一开始以为是配置原因&#xff0c;但后面尝试创建空项目都失败…… 觉得可能跟版本什么的无关&#xff0c;尝试重启、更新系统后&#xff0c;试着以管理员身份运行idea&#xff0c;问题解决了……………… 如果有报错信…

MySQL---存储函数、触发器

1. 存储函数 MySQL存储函数&#xff08;自定义函数&#xff09;&#xff0c;函数一般用于计算和返回一个值&#xff0c;可以将经常需要使用的计算 或功能写成一个函数。 存储函数和存储过程一样&#xff0c;都是在数据库中定义一些 SQL 语句的集合。 存储函数与存储过程的区…

kubernetes名称空间和资源配额

目录 什么是命名空间 namespace应用场景 namespace使用案例 namespace资源限额 什么是命名空间 Kubernetes支持多个虚拟集群&#xff0c;他们底层依赖于一个物理集群。这些虚拟机群被称为命名空间。 命名空间namespace是k8s集群级别的资源&#xff0c;可以给不同的用户、租…

期刊介绍|骨科老牌期刊,无版面费,审稿极速,毕业不二之选!

今天给大家介绍一本中药方面的期刊&#xff1a;JOURNAL OF ORTHOPAEDIC RESEARCH 一、基本信息 1、期刊名称&#xff1a;JOURNAL OF ORTHOPAEDIC RESEARCH&#xff1b; 2、期刊ISSN: 0736-0266&#xff1b; 3、研究方向&#xff1a;医学-整形外科&#xff1b; 4、出版社&#x…
最新文章