数据结构 | 第8章 - 红黑树(中)

122 阅读7分钟

8.3.2 红黑树接口定义

基于BST定义的红黑树接口:

 0001 #include "BST/BST.h" //基于BST实现RedBlack
 0002 template <typename T> class RedBlack : public BST<T> { //RedBlack树模板类
 0003 protected:
 0004    void solveDoubleRed ( BinNodePosi<T> x ); //双红修正
 0005    void solveDoubleBlack ( BinNodePosi<T> x ); //双黑修正
 0006    int updateHeight ( BinNodePosi<T> x ); //更新节点x的高度(重写)
 0007 public:
 0008    BinNodePosi<T> insert ( const T& e ); //插入(重写)
 0009    bool remove ( const T& e ); //删除(重写)
 0010 // BST::search()等其余接口可直接沿用
 0011 };

基用以简化红黑树算法描述的宏:

 0001 #define IsBlack(p) ( ! (p) || ( RB_BLACK == (p)->color ) ) //外部节点也视作黑节点
 0002 #define IsRed(p) ( ! IsBlack(p) ) //非黑即红
 0003 #define BlackHeightUpdated(x) ( /*RedBlack高度更新条件*/ \
 0004    ( stature( (x).lc ) == stature( (x).rc ) ) && \
 0005    ( (x).height == ( IsRed(& x) ? stature( (x).lc ) : stature( (x).lc ) + 1 ) ) \
 0006 )

红黑树节点的黑高度更新:

 0001 template <typename T> int RedBlack<T>::updateHeight ( BinNodePosi<T> x ) { //更新节点高度
 0002    return x->height = IsBlack ( x ) + max ( stature ( x->lc ), stature ( x->rc ) ); //孩子黑高度通常相等,除非出现双黑
 0003 }

8.3.3 节点插入算法

  • 节点插入与双红现象

    如代码8.16所示,不妨假定经调用接口search(e)做查找之后,确认目标节点尚不存在。于是,在查找终止的位置x处创建节点,并随即将其染成红色(除非此时全树仅含一个节点)。现在,对照红黑树的四项条件,唯有(3)未必满足——亦即,此时x的父亲也可能是红色。

    分析:为什么插入的节点要染成红色?因为需满足条件(4)

    代码8.16 红黑树insert()接口:

     0001 template <typename T> BinNodePosi<T> RedBlack<T>::insert ( const T& e ) { //将e插入红黑树
     0002    BinNodePosi<T> & x = search ( e ); if ( x ) return x; //确认目标不存在(留意对_hot的设置)
     0003    x = new BinNode<T> ( e, _hot, NULL, NULL, 0 ); _size++; //创建红节点x:以_hot为父,黑高度0
     0004    BinNodePosi<T> xOld = x; solveDoubleRed ( x ); return xOld; //经双红修正后,即可返回
     0005 } //无论e是否存在于原树中,返回时总有x->data == e
    

    因新节点的引入,而导致父子节点同为红色的此类情况,称作“双红”(double red) 。 为修正双红缺陷,可调用solveDoubleRed(x)接口。每引入一个关键码,该接口都可能迭代地调用多次。在此过程中,当前节点x的兄弟及两个孩子(初始时都是外部节点),始终均为黑色。

    将x的父亲与祖父分别记作p和g。既然此前的红黑树合法,故作为红节点p的父亲,g必然存在且为黑色。g作为内部节点,其另一孩子(即p的兄弟、x的叔父)也必然存在,将其记作u。以下,视节点u的颜色不同,分两类情况分别处置。

  • 双红修正(RR-1)

    u为黑色。此时x的兄弟、两个孩子的黑高度均与u相等。

    可等价于B-树,黑高度相等则同在B-树一个节点内

    此时红黑树条件(3)的违反,从B-树角度等效地看,即同一节点不应包含紧邻的红色关键码

    (a、b为两种可能情况,且存在对称情况)

    image-20220723200025253.png

    解决方法:令p与g互换颜色,对节点x、p和g及其四颗子树,做一次局部“3 + 4”重构。

    最后:a - x b - p c - g

  • 双红修正(RR-2)

    u为红色。此时u的左、右孩子非空且均为黑色,其黑高度必与x的兄弟以及两个孩子相等。

    可等价于B-树,黑高度相等则同在B-树一个节点内

    此时红黑树条件(3)的违反,从B-树角度等效地看,即该节点因超过4度而发生上溢

    (a、b为两种可能情况,且存在对称情况)

    image-20220723200025253.png

    解决方法:红节点p和u转为黑色,黑节点g转为红色,x保持红色。从图(c')B-树的角度看,等效于上溢节点的一次分裂。

    上溢的向上传播:子树根节点g转为红色之后,有可能在更高层次再次引发双红现象。

    如何解决上溢传播:可以等效地将g视作新插入的节点,分以上两类情况如法处置。这样的迭代在红黑树中至多进行O(logn)次。

    特别地,若最后一步迭代之后导致原树根的分裂,并由g独立地构成新的树根节点,则应遵照红黑树条件(1)的要求,强行将其转为黑色——如此,全树的黑高度随即增加一层。

  • 双红修正的复杂度

    image-20220724173216549.png

    节点插入之后的双红修正,累计耗时不会超过O(logn)。即便计入此前的关键码查找以及节点接入等操作,红黑树的每次节点插入操作,都可在O(logn)时间内完成。

    需要特别指出的是,只有在RR-1修复时才需做1~2次旋转;而且一旦旋转后,修复过程必然随即完成。故就全树拓扑结构而言,每次插入后仅涉及常数次调整;而且稍后将会看到,红黑树的节点删除操作亦是如此——回顾7.4节的AVL树,却只能保证前一点。

  • 双红修正算法的实现

    双红修正solveDoubleRed():

     0001 /******************************************************************************************
     0002  * RedBlack双红调整算法:解决节点x与其父均为红色的问题。分为两大类情况:
     0003  *    RR-1:2次颜色翻转,2次黑高度更新,1~2次旋转,不再递归
     0004  *    RR-2:3次颜色翻转,3次黑高度更新,0次旋转,需要递归
     0005  ******************************************************************************************/
     0006 template <typename T> void RedBlack<T>::solveDoubleRed ( BinNodePosi<T> x ) { //x当前必为红
     0007    if ( IsRoot ( *x ) ) //若已(递归)转至树根,则将其转黑,整树黑高度也随之递增
     0008       {  _root->color = RB_BLACK; _root->height++; return;  } //否则,x的父亲p必存在
     0009    BinNodePosi<T> p = x->parent; if ( IsBlack ( p ) ) return; //若p为黑,则可终止调整。否则
     0010    BinNodePosi<T> g = p->parent; //既然p为红,则x的祖父必存在,且必为黑色
     0011    BinNodePosi<T> u = uncle ( x ); //以下,视x叔父u的颜色分别处理
     0012    if ( IsBlack ( u ) ) { //u为黑色(含NULL)时
     0013       if ( IsLChild ( *x ) == IsLChild ( *p ) ) //若x与p同侧(即zIg-zIg或zAg-zAg),则
     0014          p->color = RB_BLACK; //p由红转黑,x保持红
     0015       else //若x与p异侧(即zIg-zAg或zAg-zIg),则
     0016          x->color = RB_BLACK; //x由红转黑,p保持红
     0017       g->color = RB_RED; //g必定由黑转红
     0018 ///// 以上虽保证总共两次染色,但因增加了判断而得不偿失
     0019 ///// 在旋转后将根置黑、孩子置红,虽需三次染色但效率更高
     0020       BinNodePosi<T> gg = g->parent; //曾祖父(great-grand parent)
     0021       BinNodePosi<T> r = FromParentTo ( *g ) = rotateAt ( x ); //调整后的子树根节点
     0022       r->parent = gg; //与原曾祖父联接
     0023    } else { //若u为红色
     0024       p->color = RB_BLACK; p->height++; //p由红转黑
     0025       u->color = RB_BLACK; u->height++; //u由红转黑
     0026       g->color = RB_RED; //在B-树中g相当于上交给父节点的关键码,故暂标记为红
     0027       solveDoubleRed ( g ); //继续调整:若已至树根,接下来的递归会将g转黑(尾递归,不难改为迭代)
     0028    }
     0029 }
    

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 16 天,点击查看活动详情