1. 程式人生 > >紅黑樹的理解與學習+偽代碼

紅黑樹的理解與學習+偽代碼

高度 net 更改 節點 mage 條件 兄弟節點 賦值 因子

在看HashMap源碼的時候,涉及到紅黑樹,這個數據結構早已聽聞大名,而且在學校的教材中沒有講這個數據結構,所以花了點時間去學習和理解這個數據結構。(比我想象中的復雜的多……)

Red-Black Tree的簡介

首先這是個二叉查找樹,它屬於但又不嚴格屬於平衡二叉樹(AVL),因為它沒有像平衡二叉樹一樣,嚴格規定平衡因子的絕對值要小於等於1,而是靠他的顏色規定來達到高性能。

一棵擁有n個元素的RB樹,樹的高度最多為2log(n + 1),所以操作的時間復雜度是O(logN)級別的。——所以它其實是一棵平衡查找樹。

RB-Tree的properties

首先,紅黑樹看名稱我們都知道,這是個帶顏色的樹。

紅黑樹有五個設定,或者說是規矩/性質:

  1. 每個結點,要麽是紅色,要麽是黑色。
  2. 根節點是黑色的。
  3. 每個葉節點,或者說是NIL節點(也可以說是null空結點)都是黑色的。
  4. 如果一個結點是紅色的,那麽它的爸爸不能是紅色,它的子女也不能是紅色,總之紅黑樹中不允許連續的兩個紅色結點出現。
  5. 對於每個結點,從該結點到其後代的所有葉節點的簡單路徑上,均包含相同數目的黑色節點。

還有兩個小概念:

  1. 為了方便處理邊界條件以及方便描述,我們把不需要關註的葉子節點和null節點統一用一個特殊節點表示,我們稱它為哨兵節點,記為T.NIL。
  2. 第五點性質也能用一個叫做黑高——Black height的概念來描述——從某個節點x出發(不含該節點)達到一個葉節點的任意一條簡單路徑上的黑色節點個數稱為該節點的黑高。

這裏有時候作圖方便,用兩個圈圈來表示紅色的節點,一個圈來表示黑色的節點。

下面看個紅黑圖的例子:

技術分享圖片

8節點是個紅色節點,它的黑高為1;3的黑高也是1;10的黑高也是1。

就是因為這麽苛刻看起來很奇怪的需求,讓紅黑樹有了相當不錯的性能。

紅黑樹的插入

首先,紅黑樹是一棵二叉查找樹,所以首先的插入操作和一般的插入查找樹一樣,根據查找樹的規矩將節點插入到正確的位置。

插入後,該新新節點便出現在葉子的位置,然後我們要上色——上成紅色。

為什麽是紅色呢?紅黑樹的插入的難點就是要維持住那五個性質,如果你插入的是黑色,毫無意義第5條規矩肯定會被破壞掉。而如果插入的是紅色,則存在規矩仍然成立的情況。

這個時候,我們很可能破壞了紅黑樹的規矩了,比如規矩4——連續兩個紅色節點。(其實好像也只可能破壞規矩4)

然後我們要做的,是如果發現沖突,則把這個沖突往上移動。

往上移動是總體的一個思路,相關的做法是:我們需要通過對一些節點進行從新上色,從而將破壞規矩的沖突位置往上移動,直至可以通過旋轉來解決,旋轉解決的過程中很可能伴隨著再要重新上色。

說明下,偽代碼中的節點裏的P代表是父親,然後left和right就分別代表左孩子和右孩子。

看下偽代碼:

RB-INSERT(T, x)
  y = T.NIL;
  z = T.root;
  // 一直循環直到找到z的合適位置
  while z != T.NIL
    y = z;
    if x.key < z.key
       z = z.left;
    else z = z.right;
  x.p = y;
  if y == T.NIL
     T.root = x;
  elseif x.key < y.key
     y.left = x;
  else y.right = x;
  x.left = T.NIL;
  x.right = T.NIL;


  //這上面都是二叉查找樹的插入過程,重點看下面

  //為新插入的節點上色成紅色
  x.color = RED;
  
  //如果紅色的新插入節點x的爸爸也是紅色,就和規矩四沖突了,就要調整恢復紅黑樹的五大性質
  if(x.p.color == RED)RB-INSERT-FIXUP(T, x);

插入上色後,因為x為紅色,所以只是可能與規矩4沖突,所以只要檢測到x的爸爸是紅色,就要調用恢復函數。

恢復函數的大致思路:

因為爸爸已經是紅色了嘛,然後思路就是主要研究爺爺,還有爸爸的兄弟。爺爺這裏其實已經知道了,一定是黑色的,因為爸爸是紅色的話,爺爺只能是Black。

代碼的思路是,不斷地從新節點將沖突往上走,所以首先有個循環,只要x不是root根結點,x還是紅色,循環就繼續。

然後對於每個x,我們分為兩個categoryA還有categoryB兩個大情況,其實就是x的父親是爺爺的左孩子還是右孩子,這兩個category的操作是剛好相反的,然後這裏的偽代碼就只是寫出一個。

然後每個category裏面又有三種case。

case1:

  爺爺是Black(肯定),然後叔叔是紅色的。這個時候我們只要把爺爺由黑色變成紅色,然後把叔叔和爸爸都變成黑色,這樣局部也就是從爺爺開始看下來,是沒有沖突的,但爺爺由黑色變成紅色肯定會造成上面的沖突,這裏就把沖突往上移動了。所以,下一步就是把x賦值為爺爺,繼續循環。

技術分享圖片

(圖片是借https://blog.csdn.net/lm2009200/article/details/70148565的,所以上面說的case2不關事hh)

case2:

  叔叔是黑色的,x是爸爸的右孩子,先假設爸爸是爺爺的左孩子(就假設某個category)。然後視覺上,x和爸爸的紅色沖突是“z”型的,這個時候需要的操作是對x進行左旋,然後就會把沖突變到一條直線上哈哈。

然後就可以進入case3.

技術分享圖片

(圖片是借的,所以上面的case是不一樣的hh)

case3:

  叔叔是黑色的,x是爸爸的左孩子,假設爸爸是爺爺的左孩子。這就沖突在一條線上了,這裏涉及的操作是既要旋轉也要上色,這裏也借一下大佬的圖片:

(同樣無視裏面的case的字)

技術分享圖片

要做的是,對x的爸爸進行右旋轉,然後並對x的爸爸z重上色;還有對x的爺爺a重上色。

case4是終結情況,然後x要移動到它爸爸也就是z的位置,然z不是紅色,循環結束。

這是別人寫的一個解析:

我們一步步的分析如何從左邊的圖調整為右邊的圖,首先還是回到我們的指導思想,把x指針指向節點的父節點染黑,染黑後發現改變了子樹Q和W的黑高,那麽一個做法就是右旋轉a節點,右旋節點a後發現子樹F和G的黑高加了1,破壞了性質5,那麽把節點a染紅,正好就把黑高調整回來了,經過這樣的調整,也就變成了上面右邊的紅黑樹圖案了。至此,性質4恢復了,紅黑樹的插入調整也正常結束。

然後看調整算法的偽代碼:

while(x != T.root && x.color == RED) {
    if(x.p == x.p.p.left) {
    //x的爸爸是爺爺的左孩子,categoryA
        
        y == x.p.p.right;//y是爺爺的右孩子,也就是x的叔叔
        if(y.color == red){
            //case1的情況
        
            x.p.color = black;
                  y.color = black;
                  x.p.p.color = red;
                  x = x.p.p;
        } else {
          //x的叔叔不是紅色,分成case2和case3

            if(x == x.p.right) {
            //case2——沖突成z型
        
                x = x.p;
                LEFT-ROTATE(T, x);//左旋轉操作

                //然後就變成case3
            }

            //case3 選擇加變色
            x.p.color = black;
                x.p.p.color = red;
                RIGHT-ROTATE(T, x.p.p);
        }
    
    } else(x.p為右子樹,也就是x的爸爸是爺爺的右孩子,和爸爸是左孩子的操作相反即可);
  
}
T.root.color = BLACK;//如果一查入就是根節點,就直接到這裏但根節點還是紅色,所以要變成黑色。

紅黑樹的刪除

刪除就特麽復雜了。

首先先來復習一下,二叉查找樹的刪除操作:

  如果要刪除的那個結點沒有孩子,直接刪除;如果要刪除的節點有一個左孩子或者右孩子,那麽就由這個孩子來代替它;如果要刪除的節點有兩個孩子,那麽要找它的直接前驅,它可以是左子樹的最右邊(比它小的最大值),也可以是右子樹的最左邊(比它大的最小值),找到直接前驅後,將直接前驅覆蓋到要刪除的節點的位置,然後刪除直接前驅——問題轉換到情況2甚至情況1。

紅黑樹的刪除的大致流程也和這個差不多,但它要恢復紅黑樹的五條性質。

首先我們為紅黑樹定義一個覆蓋函數:

//替換函數,用v節點替代u,只負責更改父節點的指向,左右孩子需要自己更改
RB-TRANSPLANT(T, u, v) {
    if(u.p == null) {
        T.root = v;    
    } else if(u == u.p.left) {
        u.p.left = v;    
    } else u.p.right = v;

    v.p = u.p;
}

然後是紅黑樹的刪除流程函數的偽代碼:

RB-DELETE(T, z)
  y = z;
  y-original-color = y.color;
  if z.left == T.NIL
    x = z.right;
    RB-TRANSPLANT(T, z, z.right);
  else if z.right == T.NIL
    x = z.left;
    RB-TRANSPLANT(T, z, z.left);
  else y = TREE-MINMUM(z.right)
    y-original-color = y.color;
    x = y.right;
    if y.p = z;
      x.p = y;
    else RB-TRANSPLANT(T, y, y.right)
         y.right = z.right; 
         y.right.p = y;
    RB-TRANSPLANT(T, z, y)
    y.left = z.left;
    y.left.p = y;
    y.color = z.color;//更改y的顏色,這樣的話從y以上紅黑樹的性質都不會違反 
  if y-original-color == black
    RB-DELETE-FIXUP(T, x)

一開始看這個有點繞,因為以前寫二叉查找樹的刪除,涉及替換是把要刪除的那個點的值用直接前驅的值覆蓋上去,然後改為刪除直接前驅,而這裏是直接通過指針的移動,反正如果不拿著筆仔細畫指針很容易懵。

z指的一直是要刪除的那個點,通過指針的移動後,z指的點會不可達(這裏少了free節點z的操作),也就是被刪除了;

y指的是,理論上真正要刪除的這個點,這裏就是懵的地方,後面才看清楚這裏的指針,比如本來要刪除z,然後找了z的直接前驅,理論上,直接前驅的值被覆蓋到z上,然後刪除直接前驅,所以y指向這個直接前驅。但這裏指針的操作是,直接把y指向的直接前驅變到z的位置上,z變成沒有爸爸,即不可達。所以這種情況刪除完畢,y所指的節點還在樹上;

x指向的,節點被刪除後,補上那個空位的節點。

上面的幾種刪除情況用借一個大佬的示意圖來理解:

技術分享圖片

技術分享圖片

首先,如果被刪除的那個,也就是上圖左邊y指向的那個,或者說理論上要刪除的那個節點是紅色,那麽對那五條性質不會有影響,只有這個被刪掉的y是黑色的,才需要調用下面的恢復函數。

恢復思路:

首先,被刪除的那個是黑色,那麽百分之白含有這個節點的路徑的黑高會見一,也就是肯定違背性質5,在已知這個的情況下,再分下面幾種情況(下面的情況都是已經違背了性質5):

1. 違背性質2,如果被被刪除的那個是根節點,而它的唯一一個孩子是紅色的節點,那麽就違背性質2了,這個很容易解決,直接把根節點染黑就行了。

2. 違背性質4,也就是x為紅色,他爸也是紅色,這種情況也容易解決,因為我們黑高是少了一的嘛,所以我們可以直接染黑x,這樣剛好解決問題。

3. 剩下的情況,就是只是違背了性質5了,只要調整好性質5就行了。

這裏有一個技巧,就是把x節點視為還有一層黑色,問題就變成了解決違反性質1了,也就是把x看成既紅又黑,我們只要把這層額外的黑色不斷往上推,直到推給了一個紅色節點,那麽子樹的黑高就恢復了。和插入一樣,有個關鍵思想是,轉換過程中千萬不能破壞其他任何的性質。經過分析,破壞性質1(本質上是破壞性質5)有以下五種情況:

只是違背性質5的情況下的五種情況:

以下內容全來自博客:https://blog.csdn.net/lm2009200/article/details/70162811

case 1 x是紅色的

case 2. x的兄弟節點w是紅色的

case 3 x的兄弟節點w是黑色的,而且w的兩個子節點都是黑色的

case 4 x的兄弟節點w是黑色的,w的左兒子是紅色的,w的右孩子是黑色的

case 5 x的兄弟節點w是黑色的,且w的右孩子是紅色的。

技術分享圖片

技術分享圖片

case 1是最容易解決的,直接染黑就是了。也就是說,違背性質4的情況可以和這裏歸為一類,都是直接染黑x。


case 2的話,改變w和x.p的顏色,左旋轉x.p,這樣子不改變任何性質的同時,把case 2轉變為case 3,4,5。不做詳細討論
偽代碼為:

w.color = black;

x.p.color = red;

LEFT-ROTATE(T, x.p)

w = x.p.right;

case 3的話,可以認為從x和w去掉一層黑色給x.p,如果x.p為原本為紅色的話,那麽x的子樹黑高加一,w子樹黑高不變,性質就恢復好了,如果x.p原來為黑色的,那麽認為x.p的整個子樹黑高都少了1,多了的一層黑色就給了x.p,case3就轉為case 2,3,4,5了。

偽代碼如下:

w.color = red;
x = x.p

case 4的情況左侄兒為紅,右侄兒為黑,這種情況統一轉case 5來處理。

這裏右旋w並且沒有改變紅黑樹的五大性質,轉為了case5。偽代碼如下:

w.left.color = black;
w.color = red;
RIGHT-ROTATE(T, w)
w = x.p.right;

case 5的情況是紅黑樹調整的出口,只要到達了case 5,調整完就能恢復所有性質了。調整如下圖所示:

技術分享圖片

接下來分析case5的轉換過程,這裏的思路是這樣的:首先我們要讓x子樹黑高加一,那麽就左旋轉a,左旋轉後d的左子樹沒有任何問題,但是右子樹黑高可能減少了1(如果a原來是黑色的情況),為了解決這個問題,可以把a和d顏色交換,然後染黑c,這樣左旋轉後的d的右子樹的黑高也就不會有任何改變了。偽代碼如下:

w.color = x.p.color;
x.p.color = black;
w.right.color = black;
LEFT-ROTATE(T, x.p);
x = T.root;

最後是整個刪除調整的偽代碼:

RB-DELETE-FIXUP(T, x)
 while x != T.root && x.color = black
   if x == x.p.left
     w = x.p.right
     // case 2
     if w.color = red
       w.color = black;
       x.p.color = red;
       LEFT-ROTATE(T, x.p)
       w = x.p.right;
     // case 3
     if w.left.color == black && w.right.color == black
       w.color = red;
       x = x.p;
     // case 4
     else if w.right.color == black
       w.left.color = black;
       w.color = red;
       RIGHT-ROTATE(T, w)
       w = x.p.right;
     // case 5
     w.color = x.p.color;
     x.p.color = black;
     w.right.color = black;
     LEFT-ROTATE(T, x.p);
     x = T.root;

參考文章與資料:

  網易雲公開課的算法導論紅黑樹部分。

  《必須要把紅黑樹講清楚,看完還不明白請直接找我之》

    系列2——https://blog.csdn.net/lm2009200/article/details/70148565

    系列3——https://blog.csdn.net/lm2009200/article/details/70162811

紅黑樹的理解與學習+偽代碼