红黑树也是属于一种BBST。在之前介绍的伸展树中,虽然实现简单,分摊复杂度低,但是最坏情况下的操作需要O(n)时间,无法适用于对单次效率敏感的场合。相反的,之前介绍的AVL树尽管可以保证最坏情况下的单次操作,但是要在节点中嵌入平衡因子等标识;更重要的是,删除操作之后可能需要多达O(logn)次旋转。红黑树是针对后一不足的改进。通过为节点指定颜色,合理动态调整。它可以保证:在每一次插入或删除操作之后的重平衡过程中,全树的拓扑结构更新只涉及常数个。虽然最坏情况下要涉及多达O(logn)个节点染色,但是分摊意义仅为O(1)。红黑树的“适度平衡”的标准为:任一节点左、右子树的高度相差不过两倍。(相比于AVL树放宽了平衡条件)
红黑树结构与定义
由红、黑两类节点组成的BST,统一的增设外部节点NULL,使之成为真二叉树(预处理),即统一的引入n+1个外部节点。
由红、黑两色节点组成的二叉搜索树若满足以下条件,即为红黑树。
1、树根始终为黑色。
2、外部节点均为黑色。
3、其余节点若为红色,则孩子节点必为黑色。(红之子、之父必黑)
4、从任一外部节点到根节点的沿途,黑节点的数目相等。(黑深度)
说明:根据上面的条件1和条件2可以得知,红节点均为内部节点,且其父节点及左、右孩子必然存在。条件3意味着红节点之父必为黑色,因此树中任一通路都不含相邻的红节点。从根节点通往任一节点的沿途,黑节点都不少于红节点。除去根节点本身,沿途所经过黑节点的总数称为该节点的黑深度(根节点的黑深度为0)。由条件4可进一步得知,在从任一节点通往其任一后代外部节点的沿途,黑节点总数相等。除去外部节点,沿途所经过黑节点的总数为该节点的黑高度,根节点的黑高度为全树的黑高度,在数值上与外部节点的黑深度相等。
红黑树的平衡性
与二叉搜索树一样,红黑树的性能首先取决于其平衡性。定理:N个(含扩充的外部)节点组成的红黑树T,高度h=O(logN),更严格地,有:
log2(N+1)<=h<=2log2(N+1)
证明:
左侧得到“<=”显然成立,这里证明右侧的“<=”。
若将T的黑高度记为H,则H也是T所对应(2,4)-树的高度,因此有:
H<=log2(N+1)
另一方面有任一个通路不含相邻的红节点则:
h<=2H<=log2(N+1)=O(logN)
也就是说,尽管红黑树(任一节点左、右子树的高度相差不过两倍。)不能像AVL树那样平衡,但是其高度可以保证在最小高度的两倍以内,保持了适度平衡。
红黑树接口定义
为了方便红黑树算法的描述,这里使用了相关的宏定义:
#define IsBlack(p) ( ! (p) || ( RB_BLACK == (p)->color ) ) //外部节点也视作黑节点
#define IsRed(p) ( ! IsBlack(p) ) //非黑即红
#define BlackHeightUpdated(x) ( /*RedBlack高度更新条件*/ \
( stature( (x).lc ) == stature( (x).rc ) ) && \
( (x).height == ( IsRed(& x) ? stature( (x).lc ) : stature( (x).lc ) + 1 ) ) \
)
可以根据BST模板类,派生出RedBlack模板类如下:
#include "BST/BST.h" //基于BST实现RedBlack
template class RedBlack : public BST { //RedBlack树模板类
protected:
void solveDoubleRed ( BinNodePosi(T) x ); //双红修正
void solveDoubleBlack ( BinNodePosi(T) x ); //双黑修正
int updateHeight ( BinNodePosi(T) x ); //更新节点x的高度,黑高度
public:
BinNodePosi(T) insert ( const T& e ); //插入(重写)
bool remove ( const T& e ); //删除(重写)
// BST::search()等其余接口可直接沿用
};
这里直接使用常规二叉搜索树的search()操作,并且根据红黑树的重平衡规则与算法,重写了insert()和remove()接口。
此外,此处的height不再是值常规的树高,而是红黑树的黑高度,节点的黑高度更新有三种情况:1、左、右孩子的黑高度不想等;2、作为红节点,黑高度与其孩子不想等;3、作为黑节点,黑高度不等于孩子的黑高度加1。实现如下:
template <typename T> int RedBlack<T>::updateHeight ( BinNodePosi(T) x ) { //更新节点高度
x->height = max ( stature ( x->lc ), stature ( x->rc ) ); //孩子一般黑高度相等,除非出现双黑
return IsBlack ( x ) ? x->height++ : x->height; //若当前节点为黑,则计入黑深度
} //因统一定义stature(NULL) = -1,故height比黑高度少一,好在不致影响到各种算法中的比较判断
红黑树节点插入算法实现
为了更好的理解红黑树,一定要时刻想象其影子(2,4)-树,这样可以更好的理解。
插入接口的实现如下:
template <typename T> BinNodePosi(T) RedBlack<T>::insert ( const T& e ) { //将e插入红黑树
BinNodePosi(T) & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot的设置)
x = new BinNode<T> ( e, _hot, NULL, NULL, -1 ); _size++; //创建红节点x:以_hot为父,黑高度-1
solveDoubleRed ( x ); return x ? x : _hot->parent; //经双红修正后,即可返回
} //无论e是否存在于原树中,返回时总有x->data == e
很容易知道,当插入一个节点之后会引起树的结构变化,可能会使的其不再满足红黑树的条件,这里为了说明将上述条件列出:
1、树根始终为黑色。
2、外部节点均为黑色。
3、其余节点若为红色,则孩子节点必为黑色。(红之子、之父必黑)
4、从任一外部节点到根节点的沿途,黑节点的数目相等。(黑深度)
为了说明失衡的情况,举一个实例:
现拟插入关键码e(假定目标节点不存在),按照BST的常规算法,将其插入。
此时x=insert(e)必为末端节点,假设x的父亲p=x->parent存在。下面将x染红(除非他是根),这样条件1、条件2以及条件4都可以满足,但是条件3不一定满足。(圆形红色节点,方形黑色节点,八角形不确定)例如当p为红色时就不满足,如下图所示:
此时就会出现非法的情况,将其称为双红缺陷(double_red,p->color==x->color==R)。
双红缺陷修复
还是对应于上图,为了修复双红缺陷,我们要考虑几个节点:
要根据u的颜色,分为两种情况处理:
1、RR-1:u->color==B
对应于这种情况如下图所示:
此时,x、p和g的四个孩子(可能为外部节点)全为黑,且黑高度相同。(还有对称的另两种情况)
参照AVL树中“3+4”重构,将节点x、p、g以及四颗子树按中序组合为:T0 < a < T1 < b < T2 < c < T3。
然后在进行重染色,b转黑,a或c转红。
从B-树的角度来看,调整之前之所以是非法的,是因为在某个三叉节点中插入红关键码,使得原黑关键码不在居中(RRB或BRR,出现相邻的红关键码)。调整之后的效果相当于在新的四叉节点中,三个关键码的颜色改为RBR。
2、RR-2:u->color==R
此时等同于超级节点发生上溢,如下所示(还有对称的两种情况):
此时,可以说是在四阶B-树中修复上溢。
从红黑树的角度看,就是进行一次染色,p与u转黑,g转红,如下图所示:
从B-树的角度看,修复一次上溢的过程为:节点分裂,关键码g上升一层,如下图所示:
虽然上述过程可以修复这一局部,但是也有可能继续向上传递,即g与其parent再次构成双红。
如果真的如此,可等效的将g视作新插入的节点区分上面两种情况,继续套用上述两种方法,知道所有条件满足或者抵达树根(最多迭代O(logn)次),如果真的达到树根,则应该将g恢复为黑色,整棵树的高度加1。
双红修正的复杂度分析:
重构、染色等局部操作只需要常数时间,所以只需要考虑这些操作在修正过程中被调用的总次数,可以归纳为下图情况:
上述流程图的结果即是:
因此,其中至多做O(logn)次节点染色,一次“3+4”重构操作。红黑树的每一次插入操作,都可在O(logN)时间内完成
双红修正算法实现如下:
/******************************************************************************************
* RedBlack双红调整算法:解决节点x与其父均为红色的问题。分为两大类情况:
* RR-1:2次颜色翻转,2次黑高度更新,1~2次旋转,不再递归
* RR-2:3次颜色翻转,3次黑高度更新,0次旋转,需要递归
******************************************************************************************/
template <typename T> void RedBlack<T>::solveDoubleRed ( BinNodePosi(T) x ) { //x当前必为红
if ( IsRoot ( *x ) ) //若已(递归)转至树根,则将其转黑,整树黑高度也随之递增
{ _root->color = RB_BLACK; _root->height++; return; } //否则,x的父亲p必存在
BinNodePosi(T) p = x->parent; if ( IsBlack ( p ) ) return; //若p为黑,则可终止调整。否则
BinNodePosi(T) g = p->parent; //既然p为红,则x的祖父必存在,且必为黑色
BinNodePosi(T) u = uncle ( x ); //以下,视x叔父u的颜色分别处理
if ( IsBlack ( u ) ) { //u为黑色(含NULL)时
if ( IsLChild ( *x ) == IsLChild ( *p ) ) //若x与p同侧(即zIg-zIg或zAg-zAg),则
p->color = RB_BLACK; //p由红转黑,x保持红
else //若x与p异侧(即zIg-zAg或zAg-zIg),则
x->color = RB_BLACK; //x由红转黑,p保持红
g->color = RB_RED; //g必定由黑转红
///// 以上虽保证总共两次染色,但因增加了判断而得不偿失
///// 在旋转后将根置黑、孩子置红,虽需三次染色但效率更高
BinNodePosi(T) gg = g->parent; //曾祖父(great-grand parent)
BinNodePosi(T) r = FromParentTo ( *g ) = rotateAt ( x ); //调整后的子树根节点
r->parent = gg; //与原曾祖父联接
} else { //若u为红色
p->color = RB_BLACK; p->height++; //p由红转黑
u->color = RB_BLACK; u->height++; //u由红转黑
if ( !IsRoot ( *g ) ) g->color = RB_RED; //g若非根,则转红
solveDoubleRed ( g ); //继续调整g(类似于尾递归,可优化为迭代形式)
}
}
红黑树删除算法实现
删除算法的实现如下:
template <typename T> bool RedBlack<T>::remove ( const T& e ) { //从红黑树中删除关键码e
BinNodePosi(T) & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置)
BinNodePosi(T) r = removeAt ( x, _hot ); if ( ! ( --_size ) ) return true; //实施删除
// assert: _hot某一孩子刚被删除,且被r所指节点(可能是NULL)接替。以下检查是否失衡,并做必要调整
if ( ! _hot ) //若刚被删除的是根节点,则将其置黑,并更新黑高度
{ _root->color = RB_BLACK; updateHeight ( _root ); return true; }
// assert: 以下,原x(现r)必非根,_hot必非空
if ( BlackHeightUpdated ( *_hot ) ) return true; //若所有祖先的黑深度依然平衡,则无需调整
if ( IsRed ( r ) ) //否则,若r为红,则只需令其转黑
{ r->color = RB_BLACK; r->height++; return true; }
// assert: 以下,原x(现r)均为黑色
solveDoubleBlack ( r ); return true; //经双黑调整后返回
} //若目标节点存在且被删除,返回true;否则返回false
与插入算法类似,删除一个节点同样可能使得其不在满足红黑树的定义,例如考虑下面情况。
为了删除关键码e,首先调用标准接口BST::search(e)查找。(这里假设查找成功)
首先按照BST常规算法,执行:
r=removeAT(x,_hot);//
x由孩子r接替,另一孩子记作w(即是黑的NULL)如(此时,条件1和条件2依然满足,但是条件3和条件4不一定满足。):
此时,情况易于处理,如上图所示,若x与r颜色不同,即可按上图处理就可以解决,即:若x为红色,将x替换成r之后即可复原;若x为黑色,只要在删除后将r反转成黑色即可复原。但是,如果x与r都为黑,就会出现下面情况(双黑):
此时,在删除操作之后,全树的黑深度不在统一,原B-树中x所属节点下溢,这种现象也被称为双黑现象。
双黑缺陷修复
为了修复双黑缺陷,我们要考察上图中的几个节点:
此时分为四种情况处理:
1、BB-1:s为黑,且至少有一个红色的孩子t(其余情况与下面类似)。
对t、s和p做“3+4”重构:重命名为:a、b、c。r保持黑色,a和c染黑;b继承p的原色。
对应的B-树,以上的操作等效于(通过关键码的旋转,消除超级节点的下溢):
如此,红黑树性质在全局得以恢复(删除完成)。
2、BB-2R:s为黑,且两个孩子均为黑;p为红
首先将红黑树转换成对应的B-树:
此时会发生一次下溢,但是这个时候不能向左右兄弟借出,因此采用合并操作,如下:
最后反向变换成对应的红黑树:
最后,从红黑树的角度来看,即是:r保持黑;s转红;p转黑。另外由于关键码p原为红色,所以在关键码所属的节点中,其做货有必然还有一个黑色关键码(不可能同时存在),这也就意味着,在关键码p从中取出之后不可能引发下溢。此时红黑树性质在全局得以恢复。
3、BB-2B:s为黑,且两个孩子均为黑;p为黑
同样站在B树的角度来分析:
这种情况与情况2的不同的是,由于P是黑色的,它独自一个构成超级节点,在其被借出之后会使得下溢向上传递,好在高度必上升一层,最多O(logN)步。
从红黑树的角度来看:s转红;r与p保持黑色(拓扑结构未改变)。
3、BB-3:s为红(其孩子均为黑)
同样站在B-树的角度:
经过转换zag(p)或zig(p);红s转黑,黑p转红。虽然转换之后黑高度异常,但是r有了一个新的黑兄弟s’,即转换成了上述的几种情况(由于p转红,所以只可能为,第一种和第二种情况)。于是,再只经过一轮调整就可以恢复。
复杂度分析:
其中涉及的重构、染色等局部操作,都可以在常数时间内完成,所以只用统计操作次数。用下图表示上面四种情况:
对应可解释为下图:
综合以上可以概括:
红黑树的每一删除操作都可以在O(logn)内完成,其中,至多作1、O(logn)次重染色;2、一次“3+4”重构;3、一次单旋。不难确认,一旦在某部迭代中做过节点的旋转调整,整个修复就会随即完成。与双红修正一样,双黑修正只涉及常数次的拓扑结构调整(与AVL树最本质的一项差异)。
双黑修正算法实现如下:
/******************************************************************************************
* RedBlack双黑调整算法:解决节点x与被其替代的节点均为黑色的问题
* 分为三大类共四种情况:
* BB-1 :2次颜色翻转,2次黑高度更新,1~2次旋转,不再递归
* BB-2R:2次颜色翻转,2次黑高度更新,0次旋转,不再递归
* BB-2B:1次颜色翻转,1次黑高度更新,0次旋转,需要递归
* BB-3 :2次颜色翻转,2次黑高度更新,1次旋转,转为BB-1或BB2R
******************************************************************************************/
template <typename T> void RedBlack<T>::solveDoubleBlack ( BinNodePosi(T) r ) {
BinNodePosi(T) p = r ? r->parent : _hot; if ( !p ) return; //r的父亲
BinNodePosi(T) s = ( r == p->lc ) ? p->rc : p->lc; //r的兄弟
if ( IsBlack ( s ) ) { //兄弟s为黑
BinNodePosi(T) t = NULL; //s的红孩子(若左、右孩子皆红,左者优先;皆黑时为NULL)
if ( IsRed ( s->rc ) ) t = s->rc; //右子
if ( IsRed ( s->lc ) ) t = s->lc; //左子
if ( t ) { //黑s有红孩子:BB-1
RBColor oldColor = p->color; //备份原子树根节点p颜色,并对t及其父亲、祖父
// 以下,通过旋转重平衡,并将新子树的左、右孩子染黑
BinNodePosi(T) b = FromParentTo ( *p ) = rotateAt ( t ); //旋转
if ( HasLChild ( *b ) ) { b->lc->color = RB_BLACK; updateHeight ( b->lc ); } //左子
if ( HasRChild ( *b ) ) { b->rc->color = RB_BLACK; updateHeight ( b->rc ); } //右子
b->color = oldColor; updateHeight ( b ); //新子树根节点继承原根节点的颜色
} else { //黑s无红孩子
s->color = RB_RED; s->height--; //s转红
if ( IsRed ( p ) ) { //BB-2R
p->color = RB_BLACK; //p转黑,但黑高度不变
} else { //BB-2B
p->height--; //p保持黑,但黑高度下降
solveDoubleBlack ( p ); //递归上溯
}
}
} else { //兄弟s为红:BB-3
s->color = RB_BLACK; p->color = RB_RED; //s转黑,p转红
BinNodePosi(T) t = IsLChild ( *s ) ? s->lc : s->rc; //取t与其父s同侧
_hot = p; FromParentTo ( *p ) = rotateAt ( t ); //对t及其父亲、祖父做平衡调整
solveDoubleBlack ( r ); //继续修正r处双黑——此时的p已转红,故后续只能是BB-1或BB-2R
}
}