目录
红黑树介绍
红黑树的性质:
红黑树的结点类
搜索(红黑)树的旋转
旋转分为4种(左旋,右旋,左右双旋,右左双旋):
左旋(RotateL)
右旋(RotateR)
左右双旋(RotateLR)
右左双旋(RotateRL)
红黑树的插入
插入结点(insert)
调整颜色(InsertCol)
第一种、父结点与叔结点都为红(祖结点一定为黑)
第二种、叔结点(uncle)为黑/空,插入结点(cur)在父节点(parent)的左边,父节点(parent)在祖结点(grandfather)的左边
第三种、叔结点(uncle)为黑/空,插入结点(cur)在父节点(parent)的右边,父节点(parent)在祖结点(grandfather)的左边
第四种、叔结点(uncle)为黑/空,插入结点(cur)在父节点(parent)的右边,父节点(parent)在祖结点(grandfather)的右边
第五种、叔结点(uncle)为黑/空,插入结点(cur)在父节点(parent)的左边,父节点(parent)在祖结点(grandfather)的右边
调整颜色代码(InsertCol)
insert代码总结
红黑树的删除(erase)
删除结点
1、删除的结点无子节点
2、删除的结点有一个子节点
3、删除的结点有两个子节点
调整颜色
第一种情况:删除有一个孩子的结点后调整颜色
第二种情况:删除有两个孩子的结点后调整颜色
第三种情况:删除没有孩子的红结点(红)
第四种情况:删除没有孩子的黑节点(重点)
删除代码总结(erase)
红黑树的查找
红黑树的验证
在上一期的AVL树中,这种树虽然能保持树的高度是平衡的,但还有一个缺陷那就是会导致频繁旋转,一定程度上降低了树的效率,而我们这期的红黑树解决的就是AVL树中频繁旋转的问题
红黑树底层是一颗二叉搜索树,但在结点类中还加入了一个状态用来表示结点红色还是黑色
性质1、根节点是黑色。
性质2、每个节点或者是黑色,或者是红色。
性质3、每个叶子节点(NIL)是黑色。
性质4、如果一个节点是红色的,则它的子节点必须是黑色的。
性质5、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
如图为一颗红黑树
图1
红黑树的最长路径不大于最短路径的两倍
原因是如图1:
最短路径是图上的全黑路径,最长路径是黑红相间的路径
如果一棵红黑树的最长路径大于最短路径的两倍,那么就无法满足性质5
红黑树中存储的数据是一对键值对pair,每个key都对应一个value,当我们进行查找时是通过key来找的,因为红黑树中每一个key都是唯一的
template
struct RBTreeNode
{
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col;
pair _kv;
RBTreeNode(const pair& kv)
: _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED), _kv(kv)
{
}
};
旋转是一种控制高度的方式,这种方式不仅适用于红黑树,并且适用于其他要控制高度的树,如:AVL树
如图搜索树及其发生左旋后的模样:
图2
注意:当我们在进行旋转的时候要保持其搜索树的性质不变
如图2中旋转操作改变了他们指针的指向
影响的最多有4个结点
值为4的结点
值为3的结点
值为2的结点
值为2的结点的父节点如果不为空也算一个
因为每一颗子树都是搜索树,所以如果我们不涉及改变的结点可以不管,最后只看改变了的结点即可,如图为左旋抽象图(红线标注的为需要改变的指针)
图3
如图需要改变的指针一共有六个
指针 | 原本的指向 | 左旋后的指向 |
cur的right指针 | curR | b子树 |
cur的parent指针 | parent | curR |
curR的parent指针 | cur | parent |
curR的left指针 | b子树 | cur |
b子树的parent指针 | curR | cur |
parent的left或right指针 | cur | curR |
注意:parent结点和子树结点可能为空需要判断一下
代码如下
void RotateL(Node* cur)
{
Node* curR = cur->_right;
Node* parent = cur->_parent;
Node* curRL = curR->_left;
//判断parent是否为空
if (cur == _root)
{
_root = curR;
_root->_parent = nullptr;
}
else
{
if (parent->_left == cur)
{
parent->_left = curR;
}
else
{
parent->_right = curR;
}
}
cur->_right = curRL;
cur->_parent = curR;
curR->_parent = parent;
//判断b子树是否为空
if (curRL)
curRL->_parent = cur;
curR->_left = cur;
}
右旋和左旋的区别就是:左旋是把结点往左边压,而右旋是把结点往右边压,本身都是为了实现控制树的高度。
接下来我们直接看右旋的抽象图(红线为要改变的指针)
图4
如图4,需要改变的指针也是6个
指针 | 原本的指向 | 右旋后的指向 |
cur的parent指针 | parent | curL |
cur的left指针 | curL | b子树 |
curL的right指针 | b子树 | cur |
curL的parent指针 | cur | parent |
b子树的parent指针 | curL | cur |
parent的left或right指针 | cur | curL |
左右双旋是先把双旋结点(cur)的左孩子结点进行左旋,再把双旋结点(cur)进行右旋,如图:
这种旋转直接复用左、右旋即可,代码如下
void RotateLR(Node* cur)
{
RotateL(cur->_left);
RotateR(cur);
}
右左双旋:先把双旋结点(cur)的右孩子结点进行右旋,再对双旋结点(cur)进行左旋即可
跟左右双旋一样复用代码即可,不再过多叙述
void RotateRL(Node* cur)
{
RotateR(cur->_right);
RotateL(cur);
}
红黑树的插入分为两部分,第一部分是插入结点,跟二叉搜索树一样,第二部分是调整颜色
首先,红黑树的底层是一个二叉搜索树,二叉搜索树的每一个结点的左子树都比根结点要小,每一个结点的右子树都比根要大
根据这个性质,我们每一次插入的时候把对应的数据插入到对应的位置即可
如图为一颗二叉搜索树
我们来看一下插入一个10的过程
1、10先跟根节点(19)比较,10<19,那么10就要插入到19的左子树中
2、循环到左子树的值为8的结点,此时10>8,那么10就要插入到值为8的结点的右子树中
3、循环到值为13的结点中,10<13,那么就要插入到13的左子树中,最终走到空,循环结束
注意1:二叉搜索树中插入一个数据要不就是树中有这个数据,要不就是走到空
注意2:当我们插入一个数据时需要有一个指针来存储之前结点,如上步骤我们走到13时要存储8的值。原因是如果只有一个那么这一个指针最后很可能走到空,走到空以后没办法与之前的结点再相连了
红黑树的插入跟二叉搜索树一样,只是要根据性质进行调整颜色
写成代码如下
bool insert(const pair& kv)
{
Node* cur = _root;
if (_root == nullptr)
{
//一颗树的根节点可能是空的,需要判断一下
_root = new Node(kv);
_root->_col = BLACK;
//性质1
}
else
{
Node* parent = nullptr;
while (cur)
{
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first == kv.first)
{
return false;
}
else
{
//不可能走到这
assert(false);
}
}
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
assert(false);
}
cur->_parent = parent;
InsertCol(cur);//调整颜色的接口
}
return true;
}
}
首先如果新插入的结点的父节点的颜色为黑,那么就没有什么讨论了,因为此时这个新插入的结点插入后没有改变任何的性质,所以我们下面讨论的都是parent为红的情况
注意:以下的图是抽象图,它包含了所有的情况,下图中的树可能会存在子树
调整颜色分为四种情况
解决办法:祖染父色,父叔黑,再以祖结点为cur,继续进行调整,直到遇到其他情况进行处理
染完颜色以后,其实以祖结点为根的树已经是红黑树了
如图
解决办法:右旋祖,父祖换色
如图为情况1转化为情况2:
可以看到,当情况2完成后,此时树就已经是红黑树了,可以直接跳出循环
解决办法:左右双旋其祖,子祖换色
如图为情况1转化为情况3:
此时,这颗树也为红黑树了可以跳出循环
解决办法: 左旋其祖,父祖换色
如图为情况1转化为情况4:
此时树也变成了红黑树,可以跳出循环
解决办法:右左双旋其祖,子祖换色
如图为情况1转化为情况5
void InsertCol(Node* cur)
{
Node* parent = cur->_parent;
Node* grandfather = nullptr;
//如果parent是黑色,那么插入一个红节点就不会违反性质了
while (parent && parent->_col == RED)
{
grandfather = parent->_parent;
assert(grandfather);
Node* uncle = nullptr;
//插入结点的父节点在祖结点的左边
if (parent == grandfather->_left)
{
uncle = grandfather->_right;
//第一种情况,父叔都为红
if (uncle && uncle->_col == RED)
{
grandfather->_col = RED;
uncle->_col = BLACK;
parent->_col = BLACK;
cur = grandfather;
parent = cur->_parent;
}
else
{
//第二种情况,叔结点为黑,父节点在祖结点的左边,插入结点在父节点的左边
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
//第三种情况,叔结点为黑,父节点在祖结点的左边,插入结点在父节点的左边
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
//插入结点的父节点在祖结点的右边
else
{
uncle = grandfather->_left;
//第一种情况
if (uncle && uncle->_col == RED)
{
grandfather->_col = RED;
uncle->_col = BLACK;
parent->_col = BLACK;
cur = grandfather;
parent = cur->_parent;
}
else
{
//第四种情况,叔结点为黑,父节点在祖结点的右边,插入结点在父节点的右边
if (cur == parent->_right)
{
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
//第五种情况,叔结点为黑,父节点在祖结点的右边,插入结点在父节点的左边
else
{
RotateR(parent);
RotateL(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
//根据性质1,根节点永远是黑色
_root->_col = BLACK;
}
bool insert(const pair& kv)
{
Node* cur = _root;
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
}
else
{
Node* parent = nullptr;
while (cur)
{
//查找插入的位置,parent记录上一次查看的结点
if (cur->_kv.first > kv.first)
{
parent = cur;
cur = cur->_left;
}
else if (cur->_kv.first < kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_kv.first == kv.first)
{
return false;
}
else
{
//理论上不可能走到这
assert(false);
}
}
cur = new Node(kv);
if (cur->_kv.first > parent->_kv.first)
{
parent->_right = cur;
}
else if (cur->_kv.first < parent->_kv.first)
{
parent->_left = cur;
}
else
{
assert(false);
}
cur->_parent = parent;
InsertCol(cur); //调整颜色的接口
}
return true;
}
void InsertCol(Node* cur)
{
Node* parent = cur->_parent;
Node* grandfather = nullptr;
//如果parent是黑色,那么插入一个红节点就不会违反性质了
while (parent && parent->_col == RED)
{
grandfather = parent->_parent;
assert(grandfather);
Node* uncle = nullptr;
//插入结点的父节点在祖结点的左边
if (parent == grandfather->_left)
{
uncle = grandfather->_right;
//第一种情况,父叔都为红
if (uncle && uncle->_col == RED)
{
grandfather->_col = RED;
uncle->_col = BLACK;
parent->_col = BLACK;
cur = grandfather;
parent = cur->_parent;
}
else
{
//第二种情况,叔结点为黑,父节点在祖结点的左边,插入结点在父节点的左边
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
//第三种情况,叔结点为黑,父节点在祖结点的左边,插入结点在父节点的左边
else
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
//插入结点的父节点在祖结点的右边
else
{
uncle = grandfather->_left;
//第一种情况
if (uncle && uncle->_col == RED)
{
grandfather->_col = RED;
uncle->_col = BLACK;
parent->_col = BLACK;
cur = grandfather;
parent = cur->_parent;
}
else
{
//第四种情况,叔结点为黑,父节点在祖结点的右边,插入结点在父节点的右边
if (cur == parent->_right)
{
RotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
//第五种情况,叔结点为黑,父节点在祖结点的右边,插入结点在父节点的左边
else
{
RotateR(parent);
RotateL(grandfather);
grandfather->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
//根据性质1,根节点永远是黑色
_root->_col = BLACK;
}
删除红黑树的一个结点,我们需要做的是
1、删除结点
2、调整颜色
接下来就让我们具体了解下这两个步骤吧
红黑树中删除一个结点(不考虑颜色)我们需要分三种情况
1、删除的结点无子节点
2、删除的结点有一个子节点
3、删除的结点有两个子节点
这个直接删除即可,需要把父节点指向这个结点的指针置为空,不然会出现野指针的访问
注意:当我们删除只有一个根结点的时候要把结点类中的_root置为空,不然会出现野指针
这一种情况采取的是替代法删除
首先找到一个符合前两种情况的结点可以替换当前结点,然后转化为前两种情况
如图
首先图上是一颗二叉搜索树,我们需要保持它原有的性质,替换结点一共两个
1、被删结点中左子树中的最右结点
2、被删结点中右子树中的最左结点
这两个结点跟被删除结点替换可以继续保持搜索树的性质(我们采用的是左子树中的最左结点做替换结点)
如图
此时删除绿色结点即完成删除
既然删除结点有三种情况,那么调整颜色我们暂时也分为三种情况
根据红黑树的性质,每一条路径的黑色结点个数相同,那么我们就可以得知删除有一个孩子的结点时,那个结点的孩子结点一定为红色,因为如果孩子结点为黑色的话,那么从根到这个孩子结点的路径就多出来一个黑色结点,不符合红黑树的性质
删除有一个孩子的结点时,我们直接在删除结点的基础上把孩子结点变成黑色顶替删除结点就可以
void EraseOne(Node* cur)
{
Node* child = cur->_left == nullptr ? cur->_right : cur->_left;
Node* parent = cur->_parent;
if (cur == _root)
{
_root = child;
child->_col = BLACK;
return;
}
if (parent->_left == cur)
{
parent->_left = child;
}
else
{
parent->_right = child;
}
child->_parent = parent;
child->_col = BLACK;
}
这一种情况因为是转化为删除有一个孩子或删除没有孩子的结点,所以不用特意实现
void EraseTwo(Node* cur)
{
Node* tmp = cur->_left;
while (tmp->_right)
{
tmp = tmp->_right;
}
std::swap(tmp->_kv, cur->_kv);
if (tmp->_right || tmp->_left)
{
EraseOne(tmp);
}
else
{
EraseZero(tmp);
}
}
这一种情况直接删除不会影响红黑树的性质
删除无孩子的黑色结点时,分为4种情况
注意:以下cur为删除结点,brother为cur的兄弟结点,son为brother的孩子结点
注意:以下情况是有优先级的,第一种情况不符合再执行第二种情况以此类推
第一种情况:兄黑,右红侄
如图:白色结点表示什么颜色都行
解决办法是:左旋父节点,兄染父色,侄父黑
假设父节点是黑色,如图
此时处理完后就可以整棵树都是红黑树,可以跳出循环了
第二种情况:兄黑,左红侄
这一种情况我们要转化为兄黑,右红侄进行处理
解决办法是:先右旋兄结点,兄侄换色
可以看到,此时就变成了第一种情况,循环处理即可
第三种情况:兄黑,无红侄
走到第三种情况说明兄弟节点的左右孩子为空或者都为黑色(假设都为黑结点)
解决办法:
需要看父节点的颜色,如果父节点为红色则把父结点变为黑色就调整完毕了
如果父节点为黑色,则要把兄弟结点变为红色,然后再把parent为新的cur循环向上处理
第四种情况:兄红
兄弟为红的情况也不能直接调整完毕,要转化为前面的几种情况进行处理
解决办法:左旋父结点,兄节点变为黑色,父节点变为红色,此时就从第四种情况变成了其他情况,再按其他情况的处理方式进行处理cur即可
bool erase(const K& x)
{
Node* cur = _root;
while (cur)
{
if (x > cur->_kv.first)
{
cur = cur->_right;
}
else if (x < cur->_kv.first)
{
cur = cur->_left;
}
else
{
//找到了
break;
}
}
if (cur == nullptr)
{
return false;
//没找到
}
if (cur->_left && cur->_right)
{
EraseTwo(cur);
}
else if (cur->_left == nullptr && cur->_right == nullptr)
{
EraseZero(cur);
}
else
{
EraseOne(cur);
}
//实际删除
delete cur;
cur = nullptr;
return true;
}
//删除有左右孩子的结点
void EraseTwo(Node* cur)
{
Node* tmp = cur->_left;
while (tmp->_right)
{
tmp = tmp->_right;
}
std::swap(tmp->_kv, cur->_kv);
if (tmp->_right || tmp->_left)
{
EraseOne(tmp);
}
else
{
EraseZero(tmp);
}
}
//删除有一个孩子的结点及颜色调整
void EraseOne(Node* cur)
{
Node* child = cur->_left == nullptr ? cur->_right : cur->_left;
Node* parent = cur->_parent;
if (cur == _root)
{
_root = child;
child->_col = BLACK;
return;
}
if (parent->_left == cur)
{
parent->_left = child;
}
else
{
parent->_right = child;
}
child->_parent = parent;
child->_col = BLACK;
}
//删除没有孩子的结点
void EraseZero(Node* cur)
{
if (_root == cur)
{
_root = nullptr;
return;
}
if (cur->_col == RED)
{
Node* parent = cur->_parent;
if (parent->_left == cur)
{
parent->_left = nullptr;
}
else
{
parent->_right = nullptr;
}
}
else
{
//删除无子节点且结点颜色为黑
EraseCase2(cur);
Node* parent = cur->_parent;
parent->_left == cur ? parent->_left = nullptr : parent->_right = nullptr;
}
}
//删除没有孩子的结点的子函数
void EraseCase2(Node* cur)
{
Node* parent = cur->_parent;
while (cur != _root)
{
//cur在左
if (cur == parent->_left)
{
Node* brother = parent->_right;
//兄黑,右红侄
if (brother->_col == BLACK
&& brother->_right
&& brother->_right->_col == RED)
{
RotateL(parent);
brother->_col = parent->_col;
parent->_col = brother->_right->_col = BLACK;
break;
}
//兄黑,左红侄
else if (brother->_col == BLACK
&& brother->_left
&& brother->_left->_col == RED)
{
RotateR(brother);
brother->_col = RED;
brother->_parent->_col = BLACK;
}
//兄黑
else if (brother->_col == BLACK)
{
brother->_col = RED;
if (parent->_col == RED)
{
parent->_col = BLACK;
break;
}
cur = parent;
parent = cur->_parent;
}
//兄红
else
{
RotateL(parent);
brother->_col = BLACK;
parent->_col = RED;
}
}
//cur在右
else
{
Node* brother = parent->_left;
//兄黑,左红侄
if (brother->_col == BLACK
&& brother->_left
&& brother->_left->_col == RED)
{
RotateR(parent);
brother->_col = parent->_col;
parent->_col = brother->_left->_col = BLACK;
break;
}
//兄黑,右红侄
else if (brother->_col == BLACK
&& brother->_right
&& brother->_right->_col == RED)
{
RotateL(brother);
brother->_col = RED;
brother->_parent->_col = BLACK;
}
//兄黑
else if (brother->_col == BLACK)
{
brother->_col = RED;
if (parent->_col == RED)
{
parent->_col = BLACK;
break;
}
cur = parent;
parent = cur->_parent;
}
//兄红
else
{
RotateR(parent);
parent->_col = RED;
brother->_col = BLACK;
}
}
}
}
红黑树的查找与二叉搜索树的查找一模一样
如果查找的key 大于当前结点的key,则去右子树进行查找
如果查找的key 小于当前结点的key,则去左子树进行查找
如果查找的key 等于当前结点的key,查找成功,返回结点
如果走到空树,查找失败,返回空
Node* find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_kv.first)
{
cur = cur->_right;
}
else if (key < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
如果要验证一棵树是否为红黑树,则要从红黑树的性质下手,如果一棵树满足红黑树的所有性质,则此树就为一颗红黑树
红黑树的每一条到空结点的路径上黑节点的数量相同
第一步:我们先算出一条路径上的黑节点数量,并以此数量为一个基准值
第二步:递归出每一条路径,递归的过程中把基准值传下去
第三步:当递归出一条路径时比较当前路径和基准值是否相等
红黑树没有连续的红节点
第一步:遍历每个结点
第二步:遍历的过程中判断这个结点的父节点和它的颜色是否都为红色
代码如下:
bool IsValidRBTree()
{
size_t k = 0;//每个路径的黑色结点数量
size_t blackCount = 0;//基准值
Node* cur = _root;
while (cur)
{
if (cur->_col == BLACK)
{
blackCount++;
}
cur = cur->_left;
}
return _IsValidRBTree(_root, k, blackCount);
}
bool _IsValidRBTree(Node* node, size_t k, const size_t& blackCount)
{
//检查黑色结点个数
if (node == nullptr)
{
if (k != blackCount)
{
cout << "违反性质:每条路径上的黑色结点个数相等" << endl;
return false;
}
return true;
}
if (node->_col == BLACK)
{
k++;
}
//检查有无连续的红节点
if (node->_col == RED && node->_parent && node->_parent->_col == RED)
{
cout << "违反性质:不能有连续的红节点" << endl;
return false;
}
return _IsValidRBTree(node->_left, k, blackCount)
&& _IsValidRBTree(node->_right, k, blackCount);
}