紅黑樹

紅黑樹是一種具有紅黑性質(zhì)的平衡樹。在前面章節(jié)中我們有講到過另外一種平衡樹。
AVL樹:http://www.lxweimin.com/p/fa3858234329
AVL樹講究完全平衡。一旦左右子樹高度相差2,就需要旋轉(zhuǎn)來達(dá)到平衡。旋轉(zhuǎn)的次數(shù)是O(h)。和高度相等。
紅黑樹就厲害了,只要達(dá)到近似平衡,黑高度(從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的黑色節(jié)點(diǎn)數(shù)目)左子樹和右子樹相同。

紅黑樹的基本概念和性質(zhì)

哨兵節(jié)點(diǎn) 如果一個(gè)節(jié)點(diǎn)沒有子節(jié)點(diǎn)或者沒有父節(jié)點(diǎn),則該節(jié)點(diǎn)指針屬性是NIL
一個(gè)紅黑樹是滿足下列性質(zhì)的BST:

  • (1)每個(gè)節(jié)點(diǎn)或者是黑色,或者是紅色。
  • (2)根節(jié)點(diǎn)是黑色。
  • (3)每個(gè)葉子節(jié)點(diǎn)(NIL)是黑色。
  • (4)如果一個(gè)節(jié)點(diǎn)是紅色的,則它的子節(jié)點(diǎn)必須是黑色的。
  • (5)從一個(gè)節(jié)點(diǎn)到該節(jié)點(diǎn)的子孫節(jié)點(diǎn)的所有路徑上包含相同數(shù)目的黑節(jié)點(diǎn)。
    簡單概括: 黑根,紅黑黑,黑高相同
    一顆完整的紅黑樹,如下圖所示:
    image.png

    紅黑樹,和AVL樹的區(qū)別,就在于,它總是希望通過,簡單的改變顏色,就使這棵樹,達(dá)到黑高度相同。從而平衡。實(shí)在不行就通過旋轉(zhuǎn)。所以紅黑樹的平衡方法,改變顏色,旋轉(zhuǎn)

左旋和右旋

image.png

根據(jù)上圖,我們按照《算法導(dǎo)論》的偽代碼進(jìn)行逐行解析源碼

//按照從下往上不斷平衡的原則,先搞定X的指針變化,然后在搞定Y的指針變化,最后把X,Y指針相連
//p 是父節(jié)點(diǎn)的意思,NIL 是空節(jié)點(diǎn)的意思,left和right分別是左節(jié)點(diǎn)和右節(jié)點(diǎn)的意思
//1 首先 旋轉(zhuǎn)完的x的right   x 的右節(jié)點(diǎn)指針指向y的左節(jié)點(diǎn)【b】
//2 旋轉(zhuǎn)完的【b】的父親改變了,   y.left.p = x; 【b】的父親變成x
//3 旋轉(zhuǎn)完的 Y的父節(jié)點(diǎn)也改變了,
//4 【R】節(jié)點(diǎn)的子節(jié)點(diǎn)改變了 對(duì)應(yīng)這段代碼  x.p.left = y   x.p.right = y ;
//5  最后把他兩個(gè)相連 y.left = x;     x.p =y;
LEFT-ROTATE(T, x){
        y = x.right            //  set y 設(shè)置y是x的右子樹
        x.right = y.left      // 【x】的右節(jié)點(diǎn)指針指向y的左節(jié)點(diǎn)【b】
        if (y.left != T.NIL){  //如果y的左子樹不空 【b】不是空 ,就是 【b】的父親
            y.left.p = x;     //【b】的父親改成X
        }
        y.p = x.p ;          //Y的父親是原來X的父親
        if(x.p ==T.NIL){   //如果x的父親是空的,說明x是根節(jié)點(diǎn)。
            T.root = y ;   //那么這個(gè)時(shí)候,這棵樹的根節(jié)點(diǎn),變成Y
        }else if( x.p.left ==x ){   //如果【x】是【R】的左節(jié)點(diǎn)
            x.p.left = y ;   //那么改成【R】的左節(jié)點(diǎn)是【Y】
        }else{                //如果【x】是【R】的右節(jié)點(diǎn)
             x.p.right = y ; //那么改成【R】的右節(jié)點(diǎn)是【Y】
        }
        y.left = x;
        x.p =y;
}

主要步驟就是

  • 先找到旋轉(zhuǎn)反方向的子節(jié)點(diǎn)【Y】,作為固定點(diǎn),左旋就是逆時(shí)針旋轉(zhuǎn)【X】,右旋就是順時(shí)針旋轉(zhuǎn)【X】。旋轉(zhuǎn)之后,從下到上,從X===【b】==》Y===》R(右旋轉(zhuǎn)正好左右調(diào)換)逐漸改變指針。使得指針指向正確的節(jié)點(diǎn)。右旋正好鏡像調(diào)換,所以就不寫了。
    jdk源碼中TreeMap是紅黑樹的實(shí)現(xiàn)。還有類似的TreeSet (底層是用TreeMap的Key來保存 Set 集合的元素),下面我們來看看,TreeMap中的左旋,右旋代碼:
private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

p對(duì)應(yīng)偽代碼中的【x】,【r】對(duì)應(yīng)【Y】,簡直是一模一樣的代碼(過分)。

紅黑樹的插入操作:

上偽代碼:

RB-INSERT(T, z){
        y = T.NIL ;                     // 新建節(jié)點(diǎn)“y”,將y設(shè)為空節(jié)點(diǎn)。
        x = T.root ;                   // 設(shè)“紅黑樹T”的根節(jié)點(diǎn)為“x”
        while(x!= T.NIL){                 //先循環(huán)遍歷,從根節(jié)點(diǎn)不斷往下找,直到找到插入的位置,
                                        //插入之后,肯定是葉子節(jié)點(diǎn),所以x =T.NIL ,while條件不滿足,跳出循環(huán)
            y =x ;                       //最后是插入位置Y,也就是說,插入的節(jié)點(diǎn)的父節(jié)點(diǎn)是【y】
            if(z.key<x.key){
              x =x.left ;
            }else{
              x = x.right;
            }
        }
        z.p = y ;    // 設(shè)置 “z的父親” 為 “y”
        if(y==T.NIL){
            T.root =z ; // 若父節(jié)點(diǎn)【y】是空節(jié)點(diǎn),則將【z】設(shè)為【根節(jié)點(diǎn)】
        }else if(z.key <y.key){ // 若【z】小于父節(jié)點(diǎn)【y】 那,就是y 的左節(jié)點(diǎn)
            y.left =z ;
        }else{
            y.right = z; // 若【z】大于父節(jié)點(diǎn)【y】 那,就是y 的右節(jié)點(diǎn)
        }
        z.left =T.NIL ;
        z.right =T.NIL ;
        z.color = RED ;
        RB-INSERT-FIXUP(T, z)             // 通過RB-INSERT-FIXUP對(duì)紅黑樹的節(jié)點(diǎn)進(jìn)行顏色修改以及旋轉(zhuǎn),
                                            // 讓樹T仍然是一顆紅黑樹
}

基本步驟就是先找到插入位置【y】,這個(gè)作為插入節(jié)點(diǎn)【z】的父節(jié)點(diǎn)。再判斷是 【y】節(jié)點(diǎn)的左節(jié)點(diǎn)還是右節(jié)點(diǎn),最后,插入的時(shí)候是葉子節(jié)點(diǎn),所以left.right都是空節(jié)點(diǎn)NIL。剛開始插入為了滿足規(guī)則(5).不破壞黑平衡,所以先插入的節(jié)點(diǎn),肯定是紅色的。
再看TreeMap的實(shí)現(xiàn),沒有傳入Comparator的時(shí)候

else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);    //如果map中原來有的話,替換原來的值,直接return
            } while (t != null);
        }
      //parent 就相當(dāng)于偽代碼中的y ,新建一個(gè)節(jié)點(diǎn)Entry
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);

可以發(fā)現(xiàn)代碼差不多,也是先查找插入位置,然后在和父節(jié)點(diǎn)比較大小,確定是父節(jié)點(diǎn)的左節(jié)點(diǎn)還是右節(jié)點(diǎn)。

插入操作之后的平衡操作

  • 情況說明:被插入的節(jié)點(diǎn)是根節(jié)點(diǎn)。
    處理方法:直接把此節(jié)點(diǎn)涂為黑色。
  • 情況說明:被插入的節(jié)點(diǎn)的父節(jié)點(diǎn)是黑色。
    處理方法:什么也不需要做。節(jié)點(diǎn)被插入后,仍然是紅黑樹。
  • 情況說明:被插入的節(jié)點(diǎn)的父節(jié)點(diǎn)是紅色。
    處理方法:那么,該情況與紅黑樹的“特性(5)”相沖突。這種情況下,被插入節(jié)點(diǎn)是一定存在非空祖父節(jié)點(diǎn)的;進(jìn)一步的講,被插入節(jié)點(diǎn)也一定存在叔叔節(jié)點(diǎn)(即使叔叔節(jié)點(diǎn)為空,我們也視之為存在,空節(jié)點(diǎn)本身就是黑色節(jié)點(diǎn))。理解這點(diǎn)之后,我們依據(jù)"叔叔節(jié)點(diǎn)的情況",將這種情況進(jìn)一步劃分為3種情況(Case)。
    在下面的偽代碼中,詳細(xì)講解了3種情況,該如何操作,以及為什么這么操作,使得這個(gè)紅黑樹趨于平衡。
    3中情況:
  • 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是紅色,且當(dāng)前節(jié)點(diǎn)的祖父節(jié)點(diǎn)的另一個(gè)子節(jié)點(diǎn)(叔叔節(jié)點(diǎn))也是紅色。
  • 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是紅色,叔叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是其父節(jié)點(diǎn)的右孩子
  • 當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是紅色,叔叔節(jié)點(diǎn)是黑色,且當(dāng)前節(jié)點(diǎn)是其父節(jié)點(diǎn)的左孩子
//z是插入之后的節(jié)點(diǎn),而且已經(jīng)顏色變成紅色的了
RB-INSERT-FIXUP(T, z){
    while(z.p.color ==RED){
        if(z.p ==z.p.p.left) {    //如果z的父節(jié)點(diǎn)是祖父節(jié)點(diǎn)的左孩子
            y = z.p.p.right ;    //y是z的叔叔節(jié)點(diǎn)
            //CASE1 =====================叔叔是紅色(前提條件父親節(jié)點(diǎn)是紅色的)
            /**
             * CASE 1
             *  如果叔叔也是紅色的,那么同時(shí)把叔叔和父親變成黑色的,
             *  然后祖父節(jié)點(diǎn)變成紅色的,就能使,這棵小紅黑樹,黑平衡
             *  因?yàn)槭迨搴透赣H,同時(shí)+1 ,祖父-1 ,經(jīng)過z的黑高度相當(dāng)于沒變。
              */
            if(y.color ==RED){
                 z.p.color =BLACK ;
                 y.color = BLACK;
                 z.p.p.color = RED ;
                 z = z.p.p ;  //然后祖父節(jié)點(diǎn),變成新的當(dāng)前節(jié)點(diǎn),就是說,
                            //z節(jié)點(diǎn)到祖父節(jié)點(diǎn),這棵小紅黑樹,已經(jīng)黑平衡了,從下往上逐漸通過
                            //變色或者旋轉(zhuǎn)來使整個(gè)樹,黑平衡。
            /**
             * CASE 2  (條件 y.color =BLACK && z == z.p.right)
             *  既然不屬于CASE1  ,那么 y.color ==RED 不成立,那就是說 y 是黑色的,叔叔節(jié)點(diǎn)是黑色的
             *  z是右節(jié)點(diǎn), 看下圖中  【z】節(jié)點(diǎn) =【007】  【z.p】z父親節(jié)點(diǎn) =【002】  【y】z叔叔節(jié)點(diǎn) =【014】
             *  之前 我們說過,紅黑樹新增的時(shí)候,【核心思想】:【要不斷的將破壞紅黑樹特性的紅色節(jié)點(diǎn)上移(即向根方向移動(dòng))】
             *  那么應(yīng)該是【002】節(jié)點(diǎn)為固定點(diǎn),然后【007】節(jié)點(diǎn)逆時(shí)針旋轉(zhuǎn)(左旋),把【007】轉(zhuǎn)到【002】和【011】中間
             *  ,旋轉(zhuǎn)完發(fā)現(xiàn),【002】和【007】連續(xù)兩個(gè)紅節(jié)點(diǎn),不符合紅黑樹特性(4)
             */
            }else if(z == z.p.right){
                z = z.p ;
                LEFT-ROTATE(T, z);
            }
        /**
         * CASE 3  (條件 y.color =BLACK && z == z.p.left)
         *  z是左節(jié)點(diǎn), 看下圖中 CASE3 【z】節(jié)點(diǎn) =【002】 ,經(jīng)過case2的時(shí)候z = z.p 使得z是【002】節(jié)點(diǎn)
         *  首先嘗試,改變顏色直接到達(dá)黑平衡。吧【z.p】【007】節(jié)點(diǎn)變成紅色,那經(jīng)過【007】節(jié)點(diǎn)的黑色-1 .那么
         *  把【011】節(jié)點(diǎn)變成黑色,+1。使得黑高度正負(fù)抵消,但是【011】到【015】這條路徑,黑高度又-1 ,然后通過
         *  【007】位固定點(diǎn),順時(shí)針旋轉(zhuǎn)【011】節(jié)點(diǎn),使得【007】這個(gè)黑色加入到【011】===》【015】這條路中。
         */
        z.p.color = BLACK ;
        z.p.p.color =RED ;
        RIGHT-ROTATE(T, z.p.p);
        }else{
        same as then clause with "right" and "left" exchanged   正好相反,然后交換left和right就好了
        }
    }
        root.color = BLACK;  在最后 root根節(jié)點(diǎn),變成黑色的根節(jié)點(diǎn)
}

下圖是,3種情況,如何改變顏色,或者旋轉(zhuǎn)使得趨于黑平衡。


image.png

TreeMap中的fixAfterInsertion方法,和上面的偽代碼,實(shí)現(xiàn)一模一樣 。

紅黑樹的刪除操作

紅黑樹的復(fù)雜主要在于刪除操作
主題思路基本同二叉查找樹,首先查找到這個(gè)節(jié)點(diǎn)z,然后就是執(zhí)行RB-DELETE算法,當(dāng)結(jié)點(diǎn)z有至多一個(gè)兒子時(shí),讓y=z,然后刪除結(jié)點(diǎn)y用結(jié)點(diǎn)y的兒子x(x可以是nil,這時(shí)nil.parent可指向z.parent,體現(xiàn)了哨兵的作用)去取代結(jié)點(diǎn)y,z有兩個(gè)兒子時(shí),刪除結(jié)點(diǎn)z的后繼y,并用y的唯一的兒子結(jié)點(diǎn)x取代y
上刪除操作的偽代碼:

/**
 * 主題思路基本同二叉查找樹,首先查找到這個(gè)節(jié)點(diǎn)z,
 * 然后就是執(zhí)行RB-DELETE算法,先當(dāng)做BST刪除掉節(jié)點(diǎn),刪除節(jié)點(diǎn),需要改變指針,或者需要后繼賦值
 * 算法描述:
 *  當(dāng)結(jié)點(diǎn)z有至多一個(gè)兒子時(shí),
 * 讓y=z,然后刪除結(jié)點(diǎn)y用結(jié)點(diǎn)y的兒子x(x可以是nil,
 * 這時(shí)nil.parent可指向z.parent,體現(xiàn)了哨兵的作用)去取代結(jié)點(diǎn)y,z有兩個(gè)兒子時(shí),
 * 刪除結(jié)點(diǎn)z的后繼y,并用y的唯一的兒子結(jié)點(diǎn)x取代y
 */

RB-DELETE(T, z){
    if(z.left ==T.NIL || z.right ==T.NIL){
        y= z;
    }else{
        y =  TREE-SUCCESSOR(z); // TREE-SUCCESSOR可以是左子樹的最大值,或者右子樹的最小值.
          // 【注意:】 后繼只可能最多有一個(gè)孩子,因?yàn)榧僭O(shè)TREE-SUCCESSOR算法取得是右子樹的最小值.
         // 那么假如左右子樹都有,那左子樹肯定比這個(gè)后繼節(jié)點(diǎn)小,所以所謂的后繼就不是后繼了,取左子樹最大值同理
        //所以下面是分兩種情況
    }
    if(y.left != T.NIL){
        x = y.left;
    }else{
       x = y.right ;
    }
        /**
         *  x是y的替換品,那么x肯定是個(gè)葉子節(jié)點(diǎn),只需要改變x的parent, 和y的父親節(jié)點(diǎn)的指針,
         *   y如果是y父親節(jié)點(diǎn)的左孩子,就if(y.p.left ==y) y.p.left = x; 父親節(jié)點(diǎn)的左孩子指向x(y的替換品)
         *   y如果是y父親節(jié)點(diǎn)的右孩子,就if(y.p.right ==y) y.p.right = x; 父親節(jié)點(diǎn)的右孩子指向x(y的替換品)
         */
    x.p = y.p ;   //x的parent替換為y的parent  本來是y.p===>y===>x,把y干掉之后,y.p===>x,也就是x.p = y.p
        
        /**
         * CASE1 y(被刪除的節(jié)點(diǎn))的父親節(jié)點(diǎn)是空,那么y就是根節(jié)點(diǎn),z也是根節(jié)點(diǎn),刪除
         * 之后,x頂替,成為root節(jié)點(diǎn)
         */
    if(y.p ==T.NIL){
        T.root =  x ;
        /**
         * CASE2 y(被刪除的節(jié)點(diǎn))是父親節(jié)點(diǎn)的左孩子,x頂替為y父親節(jié)點(diǎn)的左孩子
         *
         */
    }else if(y.p.left ==y){
        y.p.left = x;    //那么x替換成y.p.left   x是y一個(gè)替換品
    }else{
        y.p.right  = x;  //那么x替換成y.p.right
    }
    if( y!= z){
        z.key =  y.key; //【y】節(jié)點(diǎn)的值 賦值給 【z】節(jié)點(diǎn)
    }
    //最后
     if(y.color == BLACK){
        RB-DELETE-FIXUP(T, x)
     }
}

下面,我們來一個(gè)例子,來逐行校對(duì),上面的偽代碼。

圖0: 原圖: 需要?jiǎng)h除節(jié)點(diǎn)7,根節(jié)點(diǎn)


image.png

圖1:把y(刪除的節(jié)點(diǎn))的值,賦值給z,


image.png

圖2: 賦值過去之后,根節(jié)點(diǎn)是數(shù)字5, 然后2節(jié)點(diǎn)的右節(jié)點(diǎn),指向替代品4,4的父親指針指向原來y(5號(hào)節(jié)點(diǎn))的父親2號(hào)節(jié)點(diǎn)。


image.png

圖3:指針變換結(jié)束
image.png

刪除之后的平衡操作。

首先上偽代碼,大家跟著偽代碼一步步理解。

/**
 * 紅黑樹刪除節(jié)點(diǎn)后,重新平衡后,可能有多種平衡方法,平衡之后,樹結(jié)構(gòu)也有多樣的
 * 承接上面的RB-DELETE ,x是實(shí)際被刪除節(jié)點(diǎn)【y】的替代品,承接圖3,x 就是4號(hào)節(jié)點(diǎn),是紅色的,直接走最后一步,把x變成黑色,就好了
 *然后圖0 ,y節(jié)點(diǎn)原來是黑色的,那么根結(jié)點(diǎn)到x結(jié)點(diǎn)的路徑中的黑色結(jié)點(diǎn)數(shù)目將會(huì)減少1,需要執(zhí)行RB-DELETE-FIXUP,恢復(fù)黑平衡
 *  【既然刪除y(y是黑色),意味著減少一個(gè)黑色節(jié)點(diǎn);那么,再在該位置上增加一個(gè)黑色即可。
 *  這樣,當(dāng)我們假設(shè)"x包含一個(gè)額外的黑色",就正好彌補(bǔ)了"刪除y所丟失的黑色節(jié)點(diǎn)"】
 *  加上黑色之后,就會(huì)出現(xiàn)下面3種情況:
 *  ① 情況說明:x是“紅+黑”節(jié)點(diǎn)。
 *     處理方法:直接把x設(shè)為黑色,結(jié)束。此時(shí)紅黑樹性質(zhì)全部恢復(fù)。
 * ② 情況說明:x是“黑+黑”節(jié)點(diǎn),且x是根。
 *     處理方法:什么都不做,結(jié)束。此時(shí)紅黑樹性質(zhì)全部恢復(fù)。
 * ③ 情況說明:x是“黑+黑”節(jié)點(diǎn),且x不是根。
 *     處理方法:這種情況又可以劃分為4種子情況。這4種子情況
 *  總共有四種Case ,其中CASE1 ====》 可以轉(zhuǎn)化為CASE2 ,也可以轉(zhuǎn)化成case3 ,
 *  CASE3 ====》 可以轉(zhuǎn)化為CASE4 ;
 */
RB-DELETE-FIXUP(T, x){
    while(x!=root && x.color ==BLACK){
        if(x == x.p.left){
            w = x.p.right ; //w 是x的兄弟節(jié)點(diǎn)
        /**
         * CASE1  x是“黑+黑”節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是紅色。
         * 因?yàn)椤究偟乃悸肥前讯嘤嗟暮谏?jié)點(diǎn)往上移動(dòng)】
         * case1 左旋轉(zhuǎn)使得兄弟節(jié)點(diǎn)是黑色的。
         * 只有這樣子,才能先x父節(jié)點(diǎn)的黑色+1 (x的黑節(jié)點(diǎn),移動(dòng)其中一個(gè)到父節(jié)點(diǎn),【往上移動(dòng)】),
         * 然后x的兄弟節(jié)點(diǎn)黑色-1 ,變成紅色。
         *
         */

            if(w.color ==RED){
                w.color = BLACK;
                x.p.color = RED;
                LEFT-ROTATE(T, x.p);
                w = x.p.right;
            }
        /**
         * case 2  x是“黑+黑”節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是黑色。x的兄弟節(jié)點(diǎn)的兩個(gè)孩子都是黑色
         * 將“x中多余的一個(gè)黑色屬性上移(往根方向移動(dòng))”
         *  x是“黑+黑”節(jié)點(diǎn),我們將x由“黑+黑”節(jié)點(diǎn) 變成 “黑”節(jié)點(diǎn),多余的一個(gè)“黑”屬性移到x的父節(jié)點(diǎn)中,
         *  即x的父節(jié)點(diǎn)多出了一個(gè)黑屬性(若x的父節(jié)點(diǎn)原先是“黑”,則此時(shí)變成了“黑+黑”;
         *  若x的父節(jié)點(diǎn)原先時(shí)“紅”,則此時(shí)變成了“紅+黑”)。
         *  此時(shí),需要注意的是:所有經(jīng)過x的分支中黑節(jié)點(diǎn)個(gè)數(shù)沒變化;
         *  但是,所有經(jīng)過x的兄弟節(jié)點(diǎn)的分支中黑色節(jié)點(diǎn)的個(gè)數(shù)增加了1(因?yàn)閤的父節(jié)點(diǎn)多了一個(gè)黑色屬性)!
         *  為了解決這個(gè)問題,我們需要將“所有經(jīng)過x的兄弟節(jié)點(diǎn)的分支中黑色節(jié)點(diǎn)的個(gè)數(shù)減1”即可,
         *  那么就可以通過“將x的兄弟節(jié)點(diǎn)由黑色變成紅色”來實(shí)現(xiàn)。
         *  經(jīng)過上面的步驟(將x的兄弟節(jié)點(diǎn)設(shè)為紅色),
         *  多余的一個(gè)顏色屬性(黑色)已經(jīng)跑到x的父節(jié)點(diǎn)中。
         *  我們需要將x的父節(jié)點(diǎn)設(shè)為“新的x節(jié)點(diǎn)”進(jìn)行處理。
         *  若“新的x節(jié)點(diǎn)”是“黑+紅”,直接將“新的x節(jié)點(diǎn)”設(shè)為黑色,即可完全解決該問題;
         *  若“新的x節(jié)點(diǎn)”是“黑+黑”,則需要對(duì)“新的x節(jié)點(diǎn)”進(jìn)行進(jìn)一步處理。
         */
            if(w.left.color ==BLACK && w.right.color ==BLACK){
                w.color =RED;
                x = x.p ;

            }else {
                /**
                 * case 3  x是“黑+黑”節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是黑色。x的兄弟節(jié)點(diǎn)的左孩子是紅色,右孩子是黑色的
                 * case 3是為了case4 做準(zhǔn)備的,總而言之,case3變化之后,得呈現(xiàn)這個(gè)樣子:
                 *
                 */
                if(w.left.color ==RED && w.right.color ==BLACK){
                w.left.color = BLACK;
                w.color =RED;
                RIGHT-ROTATE(T, w)
                w =x.p.right;
                }
                /**
                 *  case 4 比較復(fù)雜,在外面解釋
                 *
                 */
                w.color = x.p.color;
                x.p.color =BLACK ;
                w.right.color =BLACK;
                LEFT-ROTATE(T, x.p);
                 x = root;
            }
        }else{
        same as then clause with "right" and "left" exchanged
        }
    }
    x.color =BLACK;
}

用一個(gè)紅黑樹的例子來講解。
先上原圖:
圖0:


image.png

注意:5節(jié)點(diǎn)后面還有兩個(gè)NIL(節(jié)點(diǎn)),哨兵節(jié)點(diǎn),到后面你會(huì)發(fā)現(xiàn),這個(gè)哨兵節(jié)點(diǎn)是有用的。

接下來,我們刪除0010節(jié)點(diǎn)。
圖1:

image.png

刪除掉之后,我們會(huì)走【RB-DELETE-FIXUP】這個(gè)算法,來讓樹保持黑平衡。
case 1 就是圖1
case1 : *x是"黑+黑"節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是紅色。(此時(shí)x的父節(jié)點(diǎn)和x的兄弟節(jié)點(diǎn)的子節(jié)點(diǎn)都是黑節(jié)點(diǎn))。* x節(jié)點(diǎn)就是NIL 節(jié)點(diǎn),就是圖中的【NULL LEAF】節(jié)點(diǎn)。從偽代碼分析中我們可以知道,case1和case3 都只是一種中間狀態(tài),都需要后續(xù)的操作,才能真正實(shí)現(xiàn)黑平衡。case2和case4 ,才能實(shí)現(xiàn)黑平衡。因?yàn)橹挥?code>case2和case4 ,把x上面的多余黑色,向上移動(dòng)到父節(jié)點(diǎn)。
接下來,走case1中的偽代碼,w代表兄弟節(jié)點(diǎn),p代表父親,LEFT-ROTATE(T, x.p);代表旋轉(zhuǎn)x的父親節(jié)點(diǎn),也就是【005】節(jié)點(diǎn)。

if(w.color ==RED){
                w.color = BLACK;
                x.p.color = RED;
                LEFT-ROTATE(T, x.p);
                w = x.p.right;
            }

經(jīng)過變化之后,如下圖:
圖3 :


image.png

case 3 x是“黑+黑”節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是黑色。x的兄弟節(jié)點(diǎn)的左孩子是紅色,右孩子是黑色的
case 3是為了case4 做準(zhǔn)備的,,在走case3 的偽代碼:

if(w.left.color ==RED && w.right.color ==BLACK){
                w.left.color = BLACK;
                w.color =RED;
                RIGHT-ROTATE(T, w)
                w =x.p.right;
 }

經(jīng)過變化之后,如下圖:
圖4 :

image.png

case 4 x是“黑+黑”節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是黑色;x的兄弟節(jié)點(diǎn)的右孩子是紅色的,x的兄弟節(jié)點(diǎn)的左孩子任意顏色。
【0011】節(jié)點(diǎn)的左孩子是個(gè)NIL(黑色)節(jié)點(diǎn)。
case 4 涉及到3條路徑的黑高度變化。
在走case4 的偽代碼:

   w.color = x.p.color; //上圖中就是把5號(hào)節(jié)點(diǎn)的顏色,給兄弟節(jié)點(diǎn)【0011】節(jié)點(diǎn)
 x.p.color =BLACK ;
  w.right.color =BLACK;
 LEFT-ROTATE(T, x.p);
  x = root;

case 4 完整的一個(gè)操作如下圖:
圖4-2 :


image.png
  • 首先旋轉(zhuǎn)之后,把 x.p.color =BLACK ; 把父親節(jié)點(diǎn)的顏色變色黑色,就是【核心思想:把黑加黑中的某個(gè)黑色,向上移動(dòng)給父親節(jié)點(diǎn)】。
  • 但是這樣子的話,根節(jié)點(diǎn)到兄弟節(jié)點(diǎn)的左孩子NULL LEAF,這條路徑多了一個(gè)黑色,然后把0011節(jié)點(diǎn)變成紅色,
  • 但是這樣的話,根節(jié)點(diǎn)到兄弟節(jié)點(diǎn)的右孩子0012節(jié)點(diǎn),少了一個(gè)黑色,那就吧0012節(jié)點(diǎn)變成黑色。最終根節(jié)點(diǎn)到3個(gè)NULL LEAF都是黑高度=2 ;

上面這個(gè)紅黑樹,刪除節(jié)點(diǎn)之后,經(jīng)過了3個(gè)情況。最終才完成黑平衡。但是并沒有經(jīng)過case2 .
然后我們?cè)賮砜纯碿ase2如何讓樹黑平衡。
x是"黑+黑"節(jié)點(diǎn),x的兄弟節(jié)點(diǎn)是黑色,x的兄弟節(jié)點(diǎn)的兩個(gè)孩子都是黑色
將“x中多余的一個(gè)黑色屬性上移(往根方向移動(dòng))”。 x是“黑+黑”節(jié)點(diǎn),我們將x由“黑+黑”節(jié)點(diǎn) 變成 “黑”節(jié)點(diǎn),多余的一個(gè)“黑”屬性移到x的父節(jié)點(diǎn)中,即x的父節(jié)點(diǎn)多出了一個(gè)黑屬性(若x的父節(jié)點(diǎn)原先是“黑”,則此時(shí)變成了“黑+黑”;若x的父節(jié)點(diǎn)原先時(shí)“紅”,則此時(shí)變成了“紅+黑”)。 此時(shí),需要注意的是:所有經(jīng)過x的分支中黑節(jié)點(diǎn)個(gè)數(shù)沒變化;但是,所有經(jīng)過x的兄弟節(jié)點(diǎn)的分支中黑色節(jié)點(diǎn)的個(gè)數(shù)增加了1(因?yàn)閤的父節(jié)點(diǎn)多了一個(gè)黑色屬性)!為了解決這個(gè)問題,我們需要將“所有經(jīng)過x的兄弟節(jié)點(diǎn)的分支中黑色節(jié)點(diǎn)的個(gè)數(shù)減1”即可,那么就可以通過“將x的兄弟節(jié)點(diǎn)由黑色變成紅色”來實(shí)現(xiàn)。
經(jīng)過上面的步驟(將x的兄弟節(jié)點(diǎn)設(shè)為紅色),多余的一個(gè)顏色屬性(黑色)已經(jīng)跑到x的父節(jié)點(diǎn)中。我們需要將x的父節(jié)點(diǎn)設(shè)為“新的x節(jié)點(diǎn)”進(jìn)行處理。若“新的x節(jié)點(diǎn)”是“黑+紅”,直接將“新的x節(jié)點(diǎn)”設(shè)為黑色,即可完全解決該問題;若“新的x節(jié)點(diǎn)”是“黑+黑”,則需要對(duì)“新的x節(jié)點(diǎn)”進(jìn)行進(jìn)一步處理。
如下圖2:

image.png

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1、紅黑樹介紹 紅黑樹又稱R-B Tree,全稱是Red-Black Tree,它是一種特殊的二叉查找樹,紅黑樹的...
    文哥的學(xué)習(xí)日記閱讀 9,901評(píng)論 1 35
  • 0.目錄 1.算法導(dǎo)論的紅黑樹本質(zhì)上是2-3-4樹 2.紅黑樹的結(jié)構(gòu)和性質(zhì) 3.紅黑樹的插入 4.紅黑樹的刪除 5...
    王偵閱讀 2,521評(píng)論 1 2
  • 1.紅黑樹簡介 紅黑樹是一種自平衡的二叉查找樹,是一種高效的查找樹。它是由 Rudolf Bayer 于1978年...
    多喝水JS閱讀 448評(píng)論 0 2
  • 紅黑樹(英語:Red–black tree)是一種自平衡二叉查找樹,是在計(jì)算機(jī)科學(xué)中用到的一種數(shù)據(jù)結(jié)構(gòu),典型的用途...
    卡巴拉的樹閱讀 15,237評(píng)論 11 113
  • 在離這很遠(yuǎn)的地方 有一片海灘 孤獨(dú)的人他就在海上 撐著船帆 如果你看到他 回到海岸 就請(qǐng)你告訴他你的名字 我的名字...
    似水榴蓮閱讀 296評(píng)論 0 0