目录
前言:
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;
else
parent->_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;
else
parent->_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;
else
parent->_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的全部理解了,希望对大家有帮助。