欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客
https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.com
https://gitee.com/bingbingsurercool
系列文章推荐
目录
前言
二叉树作为搜索树的时候效率的高低是通过树的结构决定的,当一颗二叉树是完全二叉树或者接近完全二叉树时,那么这棵树的查找效率就会处于logN的量级,效率很高。但是当二叉树中插入的数据为有序或者接近有序的时候,二叉树就会退化成单枝树,其效率就会变为O(N),所以这就需要使我们的二叉树具备保持相对平衡的能力。平衡树则有AVL树和红黑树。
1.AVL树
1.1AVL树的概念
AVL树又称高度平衡二叉搜索树是由两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年 发明了一种解决二叉树平衡的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一颗AVL树或者空树,其具备以下性质:
(1)它的左右子树都是AVL树
(2)左右子树高度之差(简称平衡因子)的绝对值不超过1(-1,0,1)
当一颗二叉搜索树为AVL树时,其搜索效率就会到达O(log(N)),具备n个节点的树的高度也会保持在log(n)。
1.2AVL树的节点
由于AVL树是通过平衡因子进行调节树的平衡的,因此AVL树中不在是简单具备数据域与左右指针域的二叉链,而是具备父亲指针的三叉链,这样就能通过父亲指针快速的访问到父节点中的平衡因子来进行调节。
平衡因子和三叉链结构并非所有的AVL树都具备,只是我们的实现方式如此,便于平衡的调节。平衡因子为右子树的高度与左子树的高度之差。
下面为AVL树中的节点内容,为了方便存储K,V结构采用pair进行保存数据域。
template<class K, class V>
struct AVLTreeNode
{
pair<K, V>_kv;
AVLTreeNode* _left;
AVLTreeNode* _right;
AVLTreeNode* _parent;
int _bf;
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
1.3AVL树的平衡因子调节
AVL树中节点的插入操作不在是简单的找寻空指针然后连接的过程,再插入新节点后需要额外考虑此时AVL树是否满足平衡状态,新插入的节点有没有破坏原有的平衡,如果破坏了原有的平衡就需要对AVL树进行调整,让其重新满足AVL树的定义。删除节点也同样具备这些要求,不能一昧的只删除不调节。
已插入为例,AVL树在插入时也是先进行查找节点位置,在找到位置后将新节点连接在parent指针指向的位置。
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)//kv大,向右找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//kv小,向左找
{
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;
插入新节点后我们需要对新节点的父亲节点进行平衡因子的更新,平衡因子的更新具备下列规则:
(1)如果新增节点在父节点的右边,父节点的平衡因子需要自增,反之在左边需要自减。
(2)平衡因子更新后,父节点的平衡因子变为1或者-1,这说明原本父节点的平衡因子为0,是满足平衡的,节点新增后父节点高度变化,需要向上一级节点继续更新。
(3)平衡因子更新后,父节点平衡因子变为0,这说明原本父节点的平衡因子为1或者-1,此次的节点正好新增在原本矮的子树中,父节点达到平衡,不会影响上一层的节点,平衡因子不需要再次向上更新。
(4)平衡因子更新后,父节点平衡因子变为2或者-2,这说明原本父节点的平衡因子为1或者-1,此次的节点正好新增在原本高的子树中,父节点的左右子树高度不在满足AVL树的特征,此时需要进行平衡调节,使其满足AVL树的条件。
(5)平衡因子更新后,父节点平衡因子大于2或者小于-2,说明前面的AVL树就不满足,平衡因子出现错误。
将上诉规则转化为代码后如下所示。
if (cur == parent->_left)//平衡因子更新
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)//平衡因子为0,说明已经平衡,不用继续向上调整
{
break;
}
else if (abs(parent->_bf) == 1)//继续向上调整
{
parent = parent->_parent;
cur = cur->_parent;
}
//判断是否旋转
else if (abs(parent->_bf) == 2)//达到旋转条件,需要旋转
{
//进行旋转操作来维持AVL树的平衡
break;//旋转后跳出
}
else//bug?
{
assert(false);
}
1.4AVL树的旋转
AVL树的平衡是通过旋转来保持的,AVL树的旋转分为4种情况,左单旋,右单旋,左右双旋,右左双旋。
1.4.1左单旋
左单旋的情况是新插入的节点插入在右子树的右侧,使其右边高,这时我们需要向左旋转,把这课子树根节点的右子树作为新的根节点,原本右子树的左子树变为根节点的右子树,从而降低右侧的高度,使其平衡。
上图中a,b,c均为高度为h的平衡树,其中h可以为0,如果root为根节点,那么旋转之后的subR就变为新的根节点,我们要更新整棵树的根节点为subR。如果root并非根节点,我们在调整完这颗子树后需要将其重新连接在整棵树中,因此我们应该将原本root节点中的父节点rootP的孩子指向改为subR,root的父节点改为subR,subR的父节点改为rootP,而原本subR的左子树subRL的父节点改为root。
总之,我们一共改变了6个指针的指向,root,subR,subRL以及他们的父指针。而在旋转后,平衡因子只有root和subR需要改变,二者的平衡因子都变为0。
void RotateL(Node* root)
{
Node* subR = root->_right;
Node* subRL = subR->_left;
Node* rootP = root->_parent;
root->_right = subRL;
if (subRL)
subRL->_parent = root;
subR->_left = root;
root->_parent = subR;
if (_root == root)//旋转的为根节点
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (root == rootP->_left)
{
rootP->_left = subR;
}
else
{
rootP->_right = subR;
}
subR->_parent = rootP;
}
//调节平衡因子
root->_bf = 0;
subR->_bf = 0;
}
1.4.2右单旋
右单选与左单选完全相反,当新节点插入到较高左子树的左侧时,将会导致左边高,我们需要进行右旋转,将根节点的左子树作为新的根节点,然后将原本左子树的右子树连接到原来的根节点的左侧。
void RotateR(Node* root)
{
Node* subL = root->_left;
Node* subLR = subL->_right;
Node* rootP = root->_parent;
root->_left = subLR;
if (subLR)
subLR->_parent = root;
subL->_right = root;
root->_parent = subL;
if (_root == root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (root == rootP->_left)
{
rootP->_left = subL;
}
else
{
rootP->_right = subL;
}
subL->_parent = rootP;
}
subL->_bf = root->_bf = 0;
}
1.4.3左右双旋
双旋的操作比较复杂,这种情况发生在新节点的插入不再是单一的在较高子树的高侧。左右双旋就是新节点插入到较高左子树的右侧。此时我们需要先进行左单旋,将高度变为单一的左子树的左侧高,在对根节点进行右旋转从而保存平衡。
我们的具体做法是将根节点的左子树subL的右子树subLR拿出来,此时subLR的左右子树高度就从原本的h变为h-1。新增节点无论是在subLR的左子树还是右子树都满足新增节点在较高左子树的右侧的条件。假设新增在subLR的左子树中,此时b的高度变为h,以subL为根节点进行左旋,将subLR作为新的根节点连接在root的左侧,将subLR的左子树b连接在subL的右子树中,旋转后的树中,subL 的左右子树高度都为h,满足平衡,而subLR的左子树为h+1,右子树为h-1,此时对该节点进行右旋,将subLR做整棵子树的根节点,root旋转到右边,并将subLR的右子树c连接在左侧。
左右双旋最难的还是平衡因子的调节,当新节点插入到b中时,平衡因子在旋转后subL的平衡因子为0,subLR为0,root则为1。
若新节点插入到c中,此时情况发生变化,旋转过后root的平衡因子为0,subLR为0,subL为-1。
更难的情况是新增节点即为subRL,此时平衡调节完毕后,三个节点的平衡因子都是0。
左右双旋的旋转逻辑直接就可以复用左单旋和右单选的代码:
void RotateLR(Node*root)
{
Node* subL = root->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(root->_left);
RotateR(root);
//平衡因子调节
subLR->_bf = 0;
if (bf == 0)//新插入节点就是subLR
{
root->_bf = subL->_bf = 0;
}
else if (bf == -1)//插入在较高子树的左
{
subL->_bf = 0;
root->_bf = 1;
}
else if (bf == 1)//插入在较高子树的右
{
subL->_bf = -1;
root->_bf = 0;
}
else
{
assert(false);
}
}
1.4.4右左双旋
右左双旋则发生在插入节点在较高右子树的左侧时进行的旋转,与左右双旋类似,该操作是先利用右旋转将右子树的右侧变高,让其满足左单旋的条件,随后进行左单旋完成平衡调节。
平衡因子的调节与上面的左右双旋类似,也需要分情况进行调节。
void RotateRL(Node* root)
{
Node* subR = root->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(root->_right);
RotateL(root);
//平衡因子调节
subRL->_bf = 0;
if (bf == 0)//新插入节点就是subLR
{
root->_bf = subR->_bf = 0;
}
else if (bf == -1)//插入在较高子树的左
{
subR->_bf = 1;
root->_bf = 0;
}
else if (bf == 1)//插入在较高子树的右
{
subR->_bf = 0;
root->_bf = -1;
}
else
{
assert(false);
}
}
1.5小结
AVL树最难的是平衡因子在旋转之后的调节,尤其是双旋操作后,我们需要根据不同的条件进行不同的调节。
我们发现当parent的平衡因子为2时,说明右侧比左侧高,cur的平衡因子为1说明新增的节点位于右子树的右侧,此时需要调用左单旋,而如果cur的平衡因子为-1,说明新增节点在右子树的左侧,此时需要进行双旋,右左双旋。当parent的平衡因子为-2时,说明左侧比右侧高,cur的平衡因子为-1说明新增的节点位于左子树的左侧,此时需要调用右单旋,而如果cur的平衡因子为1,说明新增节点在左子树的右侧,此时需要进行双旋,左右双旋。
至于双旋操作中平衡因子的调节则是根据调节前的subRL或者subLR中的平衡因子确定的,因此我们需要在调节前进行保存,然后再更改平衡因子。
旋转调用的条件如下:
if (parent->_bf == 2 && cur->_bf == 1)
{
//左单旋
RotateL(parent);
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
//右单旋
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
//先右单旋在左单旋
RotateRL(parent);
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
//先左单旋在右单旋
RotateLR(parent);
}
else//不存在
{
assert(false);
}
break;
2.红黑树
AVL树固然优秀,但是AVL树对于平衡的条件太过苛刻,这将导致AVL树中的节点调节必然会频繁进行,这就降低了AVL树的效率。基于这种情况,一些天才就发明了红黑树来代替AVL树。
2.1红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。红黑树并不是绝对的平衡树,是一种近似平衡的树。
2.2红黑树的性质
那么红黑树是通过什么方式满足平衡的条件呢?任意一颗红黑树都需要满足以下几条性质:
(1)每个结点不是红色就是黑色
(2)根节点是黑色的
(3)如果一个节点是红色的,则它的两个孩子结点是黑色的
(4)对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
(5)每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
当上面的性质统统具备时,此红黑树就达到了平衡状态。为什么满足上面的性质红黑树就能保证最长路径中节点个数不会超过最短路径节点 个数的两倍呢?
原因在于第三条和第四条性质,由于每条路径上的黑色节点都是相同的,因此最短的路径必然是全黑的节点,又因为不能存在连续的红色节点,所以最长的路径必然是一黑一红交替的,由于黑色节点数相同因此最长的路径中节点数最长也就是二倍的黑色节点数(黑色加红色),所以不会超过最短路径的两倍。
2.3红黑树的节点
红黑树的节点与AVL树的节点类似,都采用了三叉链,红黑树的节点中不具备平衡因子,红黑树的平衡是通过其性质决定的,只要任意一课子树都满足该性质,就一定是红黑树。
红黑树中节点的颜色采用枚举常量进行列出,在插入节点时,默认颜色为红色(原因下文给出)。
enum Color
{
RED,
BLACK
};
template<class K, class V>
struct RBTreeNode
{
pair<K, V>_kv;
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col;
RBTreeNode(const pair<K, V>& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _col(RED)
{}
};
实际上,库中的红黑树还增加了一个头节点,该头节点的使用是为了方便关联式容器的实现,头节点中的parent指针指向了根节点,left指向了树中最小的节点,right指向了树中最大的节点。
2.4红黑树的调节
红黑树的调节方式有两种,分别是变色和旋转。由于红黑树是为了减少旋转时的消耗进行设计的,因此红黑树的调节中尽量是能变色解决就变色解决,解决不了在进行旋转,旋转方式与AVL树的方式一样。
以红黑树的插入为例,插入过程中,随着节点的增加势必会发生变色和旋转。
红黑树的新增节点是具备颜色的,这里我们选择默认插入节点的颜色为红色,为什么呢?如果默认节点为黑色那我们就会违反规则4,新增节点的路径上黑色节点数比其他路径都多1,此时我们需要对每条路径上的节点都需要更改颜色,直到满足每条路径黑色节点都相同为止,如果新增节点为红色,我们违反了规则3,此时虽然相邻的两个节点都是红色,但是只影响当前路径,其他路径并不影响,调节的代价是比较小的。
因此红黑树的新增节点都设为红色,然后在进行调节。调节时红黑树将先进行变色处理,在进行旋转处理。 红黑树的调节关键是看叔叔节点,当叔叔节点存在且为红时,我们只需要变色处理然后继续向上调节,当叔叔不存在或者为黑时则需要旋转加变色处理。
调整结束的条件为parent节点为黑色,此时cur虽然为红单并不影响插入,因为红节点可以插入在黑节点后面,还有就是parent不存在,这说明我们调节到了根节点,此时只需要将根节点变为黑即可。
在插入新节点后,红黑树将出现下列三种情况:
约定:cur为当前节点,p为父亲节点,u为叔叔节点,g为祖父节点,并且以p在g的左侧为例进行讲解。(p在右时相反进行操作)
调整结束的条件为parent节点为黑色,此时cur虽然为红单并不影响插入,因为红节点可以插入在黑节点后面,还有就是parent不存在,这说明我们调节到了根节点,此时只需要将根节点变为黑即可。
2.4.1变色调节
变色调节有可能直接调节结束后红黑树直接就满足条件不需要旋转,有可能调节后会演变成情况2或3。
当cur节点颜色为红色,p节点也为红色,g节点为黑色,并且u节点存在且为红色时进行变色调节。
此时将分为两种情况,但是处理方式都一样。即cur就是新增节点或者cur是循环调节后的节点。此时cur为红色,我们需要将红色的父亲节点p和红色的叔叔节点u变成黑色,然后将祖父节点g变为红色节点,然后进行判断,如果此时g即为整棵树的根节点,那我们将根节点变为黑色,变色调整结束,树不需要在进行调节。如果g不是根节点那么我们需要循环继续向上处理,将cur指向g,p指向g的父节点。
if (uncle&&uncle->_col==RED)//情况1,叔叔存在且为红
{
//只需要变色
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
2.4.2变色加单旋调节
当单纯的变色不能解决时,我们就需要进行旋转处理,与AVL树一样,当新增节点出现在较高子树的同一侧时进行单旋处理。
cur为红,p为红,g为黑,u不存在或者u存在且为黑时,此时新增节点在高侧,进行变色加单旋处理。
这里和上面一样,分cur即为新增和cur为调整后的节点两种情况,处理方式都相同。
旋转过后将p变黑,g变红。
//新增节点插入在父亲的左侧---情况2(单旋)
if (cur == parent->_left)
{
RotateR(grandfather);
//父亲变黑,祖父变红
parent->_col = BLACK;
grandfather->_col = RED;
}
2.4.3变色加双旋调节
当新增的节点在高侧的另一侧时,此时单旋解决不掉,需要进行双旋处理。
此时条件和情况二类似,只是插入的新节点的位置不在高侧。cur为红,p为红,g为黑,u不存在或者u存在且为黑时,新增节点在高侧的另一侧。
处理方式是先进行一次单旋变为单侧高的情况,转化为情况二的情形,然后在进行情况二的操作。
else//插入在父亲的右侧---情况3(双旋)
{
RotateL(parent);
RotateR(grandfather);
//cur变黑,祖父变红
cur->_col = BLACK;
grandfather->_col=RED;
}
break;
在旋转完成后就不需要继续向上处理了,因此我们直接使用break跳出循环。
红黑树的插入:
bool Insert(const pair<K, V>& kv)
{
//找到节点
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (cur->_kv.first < kv.first)//kv大,向右找
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first > kv.first)//kv小,向左找
{
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 && parent->_col == RED)
{
Node* grandfather = parent->_parent;
assert(grandfather);
assert(grandfather->_col == BLACK);
if (parent == grandfather->_left)//父亲在左,叔叔在右
{
Node* uncle = grandfather->_right;
if (uncle&&uncle->_col==RED)//情况1,叔叔存在且为红
{
//只需要变色
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//情况2,3:叔叔不存在或者叔叔存在且为黑
{
//新增节点插入在父亲的左侧---情况2(单旋)
if (cur == parent->_left)
{
RotateR(grandfather);
//父亲变黑,祖父变红
parent->_col = BLACK;
grandfather->_col = RED;
}
else//插入在父亲的右侧---情况3(双旋)
{
RotateL(parent);
RotateR(grandfather);
//cur变黑,祖父变红
cur->_col = BLACK;
grandfather->_col=RED;
}
break;
}
}
else//父亲在右,叔叔在左
{
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)//情况1,叔叔存在且为红
{
//只需要变色
parent->_col = BLACK;
uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else//情况2,3:叔叔不存在或者叔叔存在且为黑
{
//插入在父亲的右侧---情况2(单旋)
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else//插入在父亲的左侧---情况3(双旋)
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
2.5小结
红黑树的效率整体是高过AVL树的,AVL树和红黑树的效率都能达到log(N),红黑树没有追求绝对的平衡,所以红黑树的旋转相比于AVL树来说减少了太多,通常插入节点后只需要单纯的变色就解决了问题。C++库中的map和set容器都是采用的红黑树进行的实现。
3.平衡树的平衡判断
AVL树和红黑树创建完毕后我们是需要对其进行平衡判断的,这样才能确定是否成功构建了平衡树。AVL树的平衡判断采用的是高度判别,即我们找到每一个根节点的左子树长度和右子树长度,然后判断是否与根节点的平衡因子相同且小于2(右子树减左子树),满足就是AVL树,不满足就不是平衡树。
bool IsBalance()
{
return _is_balance(_root);
}
bool _is_balance(Node* root)
{
if (root == nullptr)
return true;
int hL = hight_tree(root->_left);
int hR = hight_tree(root->_right);
int hight = hR - hL;
if (hight != root->_bf)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return abs(hight) < 2
&& _is_balance(root->_left)
&& _is_balance(root->_right);
}
红黑树的平衡判断则需要采用验证规则的方式,逐条排查是否满足红黑树的规则,当规则不满足时就不平衡。
bool IsBalance()
{
if (_root == nullptr)
return true;
if (_root->_col == RED)
{
cout << "根节点不为黑" << endl;
return false;
}
int nums = 0;
return _IsBalance(_root,nums,0);
}
bool _IsBalance(Node* root,int& nums,int count)
{
if (root == nullptr)
{
if (nums == 0)
{
nums = count;
return true;
}
else if (nums == count)
{
return true;
}
else
{
cout << "路径黑色节点不相等" << endl;
return false;
}
}
if (root->_col == RED && root->_parent->_col==RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
count++;
}
return _IsBalance(root->_left, nums, count)
&& _IsBalance(root->_right, nums, count);
}
4.map和set的底层实现
map和set的底层就是红黑树,但并不是我们现在实现的红黑树,我们需要对其进行改造。我们知道set是存储单一k值的模型,主要是判断某个元素在与不在,而map是存储k-v模型的键值对,不仅可以找到某个元素在与不在,该元素还会对应单一的一种映射关系。而这两种容器的底层实现统统采用了一颗树进行封装,不论是k类型还是k-val类型,底层的红黑树都可以实现。
4.1 红黑树的改造
为了实现这一特性,我们的红黑树需要做出下列改变。红黑树节点中的数据域类型不再是pair类型,而是一种泛型类型,采用模板的形式进行传递和实现。红黑树创建时将接受三个模板参数,一个是关键字K类型,一个是传递给节点数据的T类型,另一个是仿函数。(库中还有内存管理器)
set在封装红黑树时传递了三个模板参数,分别是关键字类型的K,数据类型的K,以及仿函数KOfT。由于set只有K,因此红黑树实际接受的关键字类型是K,数据类型也是K。
而map不同,map传递给红黑树的关键字类型是K,但是传递给红黑树的数据类型并不是单独的V类型而是存储K-V类型的键值对pair。因此二者显示传递的仿函数的功能就是获取数据类型中的比对类型。因为库中实现的键值对pair类型的比对并不是单纯的按照关键字进行比对,因此需要我们显示传递。
此时我们肯定有疑惑,为啥红黑树节点中存储的数据对于set来说是K对于map来说是键值对,那我们还要单独的传递K类型的模板参数呢? 对于set确实可以,但是map却不行,因为map中有些功能函数明确要求了返回K类型,例如find和erase。
我们之前实现的红黑树的插入函数也不能符合要求,对于map来说,我们插入后不是要得知是否插入成功,我们可能还需要针对插入的数据的key类型来更改val数据,因此我们需要得到val的引用。那简单,我们改成k-val的键值对就行了呀。
实际上红黑树的返回形式确实是一个pair类型,但是并不是单独的K-V的键值对类型,而是一个迭代器和布尔值组成的pair类型。当数据插入成功时,返回的是新插入的数据的迭代器,以及true,插入失败,则返回的是原来位置数据的迭代器和false。此时我们就可以很简单的实现对K对应的数据V的更改。
而由于set和map的比较逻辑并不同,红黑树节点中数据存储的是K类型或者K-V类型的键值对,对于set类型,可以直接比对,但是map需要使用pair的first进行比较。此时针对set和map的不同,我们传入的仿函数就起了作用,对于set的仿函数,返回K比较,对于map的仿函数,返回pair的first比较。
//set仿函数
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
//map仿函数
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
此时我们的红黑树的插入改造基本就完成了。
map不仅支持insert,map还支持[ ]的重载,在insert实现后,[ ]的重载就容易了,我们只需要对insert进行调用即可。
4.2红黑树的迭代器
map和set都支持迭代器的实现,其底层还是调用了红黑树的迭代器,而红黑树的迭代器是利用节点指针进行的封装。
迭代器中的常规函数并没有什么难点,与链表的迭代器类似,但是++和--操作却不能单纯的移动指针。
红黑树的++和--操作返回的都是中序遍历的节点,++指向中序遍历的后一个节点,--指向中序遍历的前一个节点,这样保证了数据的有序性。
基于这种形式,我们的二叉树的++,--操作采用的都是非递归的遍历思路。
4.2.1operator++
对于++操作,中序遍历的方式是左子树,根,右子树。当前节点访问结束后需要访问的下一个节点是比当前节点大的数据,而对于一颗搜索树的任意一个节点来说,右子树如果存在就一定比当前节点大,因此我们需要找到右子树中最小的节点,右子树中最小的节点一定是最左边的节点,所以我们需要循环找到最左边的节点为++操作后的节点。
而右子树如果不存在,此时我们需要分情况讨论了,如果当前节点是父节点的左子树那么++操作访问的下一个节点就是父节点;如果当前节点是父节点的右子树,此时我们不能单纯的向上走,因为只有父节点访问过了才会来到右节点,此时我们需要找到一个祖先节点,且该祖先节点是父节点的左孩子,只有左孩子的父节点不会被访问,因此我们访问该节点的父节点。例如上图中的7节点,当7进行++操作,将一路向上回退,当访问到1时,发现1为父节点8的左子树,此时说明8没有被访问,我们返回8节点。
这里需要注意当我们从右边回退到根节点时,根节点的父节点是nullptr,此时说明右子树中所有的节点都访问结束了,这说明整棵树都遍历结束了,我们直接返回nullptr即可。
Self& operator++()
{
if (_node->_right)
{
Node* left = _node->_right;
while (left->_left)
{
left = left->_left;
}
_node = left;
}
else
{
Node* parent = _node->_parent;
Node* cur = _node;
while (parent && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
4.2.2operator--
--操作基本上是和++操作相反的操作,当我们的节点向后回退时,需要先判断当前节点的左子树是否村在,左子树如果存在,那么左子树中最大的节点就是仅次于当前节点的值,因此我们需要返回该节点。如果左子树不存在,此时需要判断当前节点的状态,如果当前节点为右子树,则说明根节点一定是比当前节点的值小,因此直接返回当前节点的父节点即可。但是如果当前节点为左子树,我们此时需要找到最近的一个祖先节点并且此祖先节点应该为该节点父节点的右孩子,此时返回该节点的父节点。例如上面的22,当22进行--操作时,我们不能回退到25,因为22为25的左孩子,我们需要继续向上寻找,此时25为17的右孩子,说明我们找到了,直接返回25的父节点17。
Self& operator--()
{
if (_node->_left)
{
Node* right = _node->_left;
while (right->_right)
{
right = right->_right;
}
_node = right;
}
else
{
Node* parent = _node->_parent;
Node* cur = _node;
while (parent && cur == parent->_left)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
红黑树源码中begin和end的实现采用的是头节点的左右指针指向的节点,我们的红黑树没有头节点,因此begin需要自己找寻,end相对简单,采用的时nullptr指针。
begin指向的是树中最小的节点,因此直接一直去左树中寻找即可。
iterator begin()
{
Node* left = _root;
while (left && left->_left)
{
left = left->_left;
}
return iterator(left);
}
iterator end()
{
return iterator(nullptr);
}
map和set的迭代器无非是对红黑树的一层封装。
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;
iterator begin()
{
return _t.begin();
}
iterator end()
{
return _t.end();
}
总结
map和set的封装仅实现了部分函数,我们实现的目的不是创造工具而是更好的学习工具,工欲善其事必先利其器,map和set是常用的一些存储和计数的容器,熟练运用就行。