文章目录
- 什么是红黑树
- 红黑树的性质
- 插入
- 叔叔存在且为红
- 叔叔存在且为黑或叔叔不存在
- 调整总结
- 右旋
- 左旋
- 旋转总结
什么是红黑树
红黑树也是一种二叉搜索树,只不过给这棵树上的节点带上了颜色,但是已经有了AVL树为什么还要搞出红黑树这个东西呢?首先AVL树和红黑树是同一量级的,查找一个值事假复杂度相同,但AVL树而言,控制严格平衡需要付出很大的代价,在插入和删除时需要进行大量的旋转
红黑树的性质
1.任意节点的颜色不是红色就是黑色
2.树中不允许父子节点之间颜色不允许同时为红
3.根节点一定为黑色
4.从某一节点到叶子节点的任意路径,黑色节点个数相同
5.任意叶子结点为黑色,这里的叶子结点是指空节点
插入
对于新插入的节点,我们默认设置它的颜色为红,若我们把它设为黑色,那么就一定会违反红黑树的规则,导致各路径黑色节点数目不相同,就一定需要调整,而设为红的,对于父亲为黑的这种情况,甚至不用对该树进行调整。注意红黑树的路径个数是到空节点的个数,空节点默认为黑色
下图树的路径个数为4
对于红黑树的插入操作,当前节点是否需要调整是看父亲节点的颜色,而要怎么进行调整则是看叔叔节点的颜色,对于违反红黑树性质的几种情况及其调整方案如下
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
叔叔存在且为红
对于父亲为红,且叔叔存在且为红的情况,我们的处理方法是让父亲和叔叔都变黑,让爷爷变红,如果爷爷是根节点那么就让爷爷也变为黑,如果爷爷是子树且爷爷的父亲为红色,那么就要继续向上调整。
if (uncle && uncle->_col == RED)
{
// 变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上处理
cur = grandfather;
parent = cur->_parent;
}
叔叔存在且为黑或叔叔不存在
如果父亲为红,且叔叔存在并为黑那么cur节点一定不是新插入的节点,因为如果cur是新插入的节点,那么对于原来的树上图的p为红u为黑,最左边路径只有一个黑色节点,而右边至少已经有两个黑节点了,就不满足各路径黑色节点个数相同的情况了,因此cur节点一定不是新插入的节点。
如果叔叔不存在,那么cur节点一定是新插入的节点,因为如果cur节点不是新插入的节点的话那么他这条路径上之前一定会有一个黑色节点+空黑节点,而叔叔那条路径上只有空的黑节点,这样就会违反各路径黑色节点个数相同的规则,因此cur一定是新插入的节点。
对于叔叔存在且为黑和叔叔不存在的这两种情况,我们可以合并成一种,因为他们的处理方式相同。因为此时如果不想让由连续的红色节点,就一定需要有一个染黑,而只要有一个染黑就会导致各路径上的黑色节点不相同,因此对于这两种情况我们已经不再是简单的染色问题了,而是要进行旋转且保证该树仍满足搜索二叉树的性质。
对于平衡二叉搜索树(AVL树),如何旋转的方式我们是通过平衡因子来判断的,而对于红黑树而言,它的节点中不包含平衡因子的,所以我们要通过当前节点的位置来判断,分为两种该节点是在父亲的左边和右边
对于单旋来说,节点的颜色改变是:爷爷变红,父亲变黑
首先上面的cur节点位置都是位于左左,然后对于叔叔存在且为黑和叔叔不存在这两种情况,处理方式相同都是进行右旋。同理如果新插入节点是位于右右,对于叔叔存在且为黑和叔叔不存在这两种情况,处理方式相同都是进行左旋。
cur节点相对于爷爷位于左右,当叔叔存在且为黑或者叔叔不存在可归为一类,都是以父亲节点为中心先进行右旋,在第一次右旋之后就将问题转化为cur节点位于爷爷的左左,且叔叔节点不存在或者叔叔节点存在且为黑的情况了,此时在对转化后的cur节点进行右旋,就可得到最终结果。
对于双旋来说,节点颜色的调整是:cur节点变黑,爷爷节点变红
注意双旋,在第一次旋转时并没有去改变任何节点的颜色,而是把旋转后的情况转化为一种需要单旋情况,然后在对这次单旋的结果进行颜色调整,仍是将爷爷节点变红,父亲节点变黑,只不过此时的此时的cur已经不是第一次旋转前的cur了,原来的cur已经变成现在cur的父亲节点了
双旋后要对颜色的调整动作一致,以左右双旋为例
// g
// p
// c
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
同理若cur节点位于爷爷的右左,那么我们就要先对父亲节点进行右旋,旋转之后转化为cur节点位于爷爷的右右,此时再对父亲节点进行左旋就可得到最终结果。
调整总结
无论是叔叔存在且为红还是叔叔不存在或者叔叔存在且为黑这三种情况中的任何一个,我们首先需要做的就是要去判断叔叔的位置,是在爷爷节点的左边还是爷爷节点的右边,因此我们在对节点进行调整的时候,可以分为两大类,一类是叔叔在爷爷的左边,另一类是叔叔在爷爷的右边,而要怎么调整,我们又可以在每个大类里面分为两个小类,即叔叔存在且为红或叔叔不存在和叔叔存在且为黑,此时又要判断当前节点位于父亲节点的哪一侧。上述与AVL树类似,就是cur节点相对于爷爷是位于左左,右右,左右还是右左这四种情况进行调整。旋转方式与AVL基本相同,不同的是红黑树修改节点的颜色,AVL树修改节点的平衡因子
右旋
void RotateR(Node* parent)
{
++_rotateCount;
Node* cur = parent->_left;
Node* curright = cur->_right;
parent->_left = curright;
if (curright)
curright->_parent = parent;
Node* ppnode = parent->_parent;
cur->_right = parent;
parent->_parent = cur;
if (ppnode == nullptr)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
}
左旋
void RotateL(Node* parent)
{
++_rotateCount;
Node* cur = parent->_right;
Node* curleft = cur->_left;
parent->_right = curleft;
if (curleft)
{
curleft->_parent = parent;
}
cur->_left = parent;
Node* ppnode = parent->_parent;
parent->_parent = cur;
if (parent == _root)
{
_root = cur;
cur->_parent = nullptr;
}
else
{
if (ppnode->_left == parent)
{
ppnode->_left = cur;
}
else
{
ppnode->_right = cur;
}
cur->_parent = ppnode;
}
}
对于修改颜色问题我们统一放在旋转之后进行修改的,这样做更方便对颜色进行修改,因为我们已经在外部定义过cur,parent,grandfather了,不必在旋转函数里进行找这几个节点了。
无论是左旋还是右旋我们发现父亲节点是一定要变黑的
例如:
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
旋转总结
对于AVL树,是通过平衡因子,即树的高度来判断的,左边高就要向右旋转进行平衡,相反右边高就要向左边旋转进行平衡。而对于红黑树而言,高度其实就是连续红节点,左子树右连续红节点那么就是左边高,所以我们要向右旋转,若右边有连续红节点,我们就要进行左旋。AVL树和红黑树旋转方式相同,只不过一个是通过平衡因子来判断树的高度,一个是通过连续红节点来判断高度
对于类似这两种情况,既然会出现,就证明各路径黑色节点数目一定是相同的,因此我们可以判定图一的a,b,c一定不为空且一定包含黑色节点,图二也是如此。
对于AVL树和红黑树又一个共同点就是在旋转时,如果是左右就要先去旋转转化为左左,对于右左就要先去旋转转化为右右