1. 程式人生 > >一步一圖一程式碼,一定要讓你真正徹底明白紅黑樹(平衡二叉樹)

一步一圖一程式碼,一定要讓你真正徹底明白紅黑樹(平衡二叉樹)

一步一圖一程式碼,一定要讓你真正徹底明白黑樹

作者:July   二零一一年一月

-----------------------------

本文參考:
I、  The Art of Computer Programming Volume I
II、 Introduction to Algorithms, Second Edition
III、The Annotated STL Sources
IV、 Wikipedia
V、  Algorithms In C Third Edition

VI、 本人寫的關於紅黑樹的前三篇文章:

---------------------------------------------
前言:
1、有讀者反應,說看了我的前幾篇文章,對紅黑樹的瞭解還是不夠透徹。
2、我個人覺得,如果我一步一步,用圖+程式碼來闡述各種插入、刪除情況,可能會更直觀易懂。
3、既然寫了紅黑樹,那麼我就一定要把它真正寫好,讓讀者真正徹底明白紅黑樹。

本文相對我前面紅黑樹相關的3篇文章,主要有以下幾點改進:
1.圖、文字敘述、程式碼編寫,彼此對應,明朗而清晰。
2.巨集觀總結,紅黑樹的性質與插入、刪除情況的認識。
3.程式碼來的更直接,結合圖,給你最直觀的感受,徹底明白紅黑樹。

ok,首先,以下幾點,你現在應該是要清楚明白了的:
I、紅黑樹的五個性質:
1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。

II、紅黑樹插入的幾種情況:
情況1,z的叔叔y是紅色的。
情況2:z的叔叔y是黑色的,且z是右孩子
情況3:z的叔叔y是黑色的,且z是左孩子

III、紅黑樹刪除的幾種情況。
情況1:x的兄弟w是紅色的。
情況2:x的兄弟w是黑色的,且w的倆個孩子都是黑色的。
情況3:x的兄弟w是黑色的,且w的左孩子是紅色,w的右孩子是黑色。
情況4:x的兄弟w是黑色的,且w的右孩子是紅色的。

除此之外,還得明確一點:
IV、我們知道,紅黑樹插入、或刪除結點後,
可能會違背、或破壞紅黑樹的原有的性質,
所以為了使插入、或刪除結點後的樹依然維持為一棵新的紅黑樹,
那就要做倆方面的工作:
1、部分結點顏色,重新著色
2、調整部分指標的指向,即左旋、右旋。

V、並區別以下倆種操作:
1)紅黑樹插入、刪除結點的操作,RB-INSERT(T, z),RB-DELETE(T, z)
2).紅黑樹已經插入、刪除結點之後,
為了保持紅黑樹原有的紅黑性質而做的恢復與保持紅黑性質的操作。
如RB-INSERT-FIXUP(T, z),RB-DELETE-FIXUP(T, x)

以上這5點,我已經在我前面的2篇文章,都已闡述過不少次了,希望,你現在已經透徹明瞭。

---------------------------------------------------------------------

本文,著重圖解分析紅黑樹插入、刪除結點後為了維持紅黑性質而做修復工作的各種情況。
[文各種插入、刪除的情況,與我的第二篇文章,紅黑樹演算法的實現與剖析相對應]

ok,開始。
一、在下面的分析中,我們約定:
要插入的節點為,N
父親節點,P
祖父節點,G
叔叔節點,U
兄弟節點,S

如下圖所示,找一個節點的祖父和叔叔節點:
node grandparent(node n)     //祖父

{
     return n->parent->parent;
 }
 
 node uncle(node n)              //叔叔

{
     if (n->parent == grandparent(n)->left)
         return grandparent(n)->right;
     else
         return grandparent(n)->left;
 }

二、紅黑樹插入的幾種情況
情形1: 新節點N位於樹的根上,沒有父節點
void insert_case1(node n) {
     if (n->parent == NULL)
         n->color = BLACK;
     else
         insert_case2(n);
 }

情形2: 新節點的父節點P是黑色
void insert_case2(node n) {
     if (n->parent->color == BLACK)
         return; /* 樹仍舊有效 */
     else
         insert_case3(n);
 }

 
情形3:父節點P、叔叔節點U,都為紅色,
[對應第二篇文章中,的情況1:z的叔叔是紅色的。]
void insert_case3(node n) {
     if (uncle(n) != NULL && uncle(n)->color == RED) {
         n->parent->color = BLACK;
         uncle(n)->color = BLACK;
         grandparent(n)->color = RED;
         insert_case1(grandparent(n));   //因為祖父節點可能是紅色的,違反性質4,遞迴情形1.
     }
     else
         insert_case4(n);   //否則,叔叔是黑色的,轉到下述情形4處理。

此時新插入節點N做為P的左子節點或右子節點都屬於上述情形3,上圖僅顯示N做為P左子的情形。

情形4: 父節點P是紅色,叔叔節點U是黑色或NIL; 
插入節點N是其父節點P的右孩子,而父節點P又是其父節點的左孩子。
[對應我第二篇文章中,的情況2:z的叔叔是黑色的,且z是右孩子]
void insert_case4(node n) {
     if (n == n->parent->right && n->parent == grandparent(n)->left) {
         rotate_left(n->parent);
         n = n->left;
     } else if (n == n->parent->left && n->parent == grandparent(n)->right) {
         rotate_right(n->parent);
         n = n->right;
     }
     insert_case5(n);    //轉到下述情形5處理。

情形5: 父節點P是紅色,而叔父節點U 是黑色或NIL,
要插入的節點N 是其父節點的左孩子,而父節點P又是其父G的左孩子。
[對應我第二篇文章中,情況3:z的叔叔是黑色的,且z是左孩子。]
void insert_case5(node n) {
     n->parent->color = BLACK;
     grandparent(n)->color = RED;
     if (n == n->parent->left && n->parent == grandparent(n)->left) {
         rotate_right(grandparent(n));
     } else {
         /* 反情況,N 是其父節點的右孩子,而父節點P又是其父G的右孩子 */
         rotate_left(grandparent(n));
     }
 }

三、紅黑樹刪除的幾種情況
上文我們約定,兄弟節點設為S,我們使用下述函式找到兄弟節點:
struct node * sibling(struct node *n)  //找兄弟節點
{
        if (n == n->parent->left)
                return n->parent->right;
        else
                return n->parent->left;
}

情況1: N 是新的根。
void
delete_case1(struct node *n)
{
        if (n->parent != NULL)
                delete_case2(n);
}

 
情形2:兄弟節點S是紅色
[對應我第二篇文章中,情況1:x的兄弟w是紅色的。]
void delete_case2(struct node *n)
{
        struct node *s = sibling(n);
 
        if (s->color == RED) {
                n->parent->color = RED;
                s->color = BLACK;
                if (n == n->parent->left)
                        rotate_left(n->parent);  //左旋
                else
                        rotate_right(n->parent);
        } 
        delete_case3(n);
}

情況 3: 兄弟節點S是黑色的,且S的倆個兒子都是黑色的。但N的父節點P,是黑色。
[對應我第二篇文章中,情況2:x的兄弟w是黑色的,且兄弟w的倆個兒子都是黑色的
(這裡,父節點P為黑)]
void delete_case3(struct node *n)
{
        struct node *s = sibling(n);
 
        if ((n->parent->color == BLACK) &&
            (s->color == BLACK) &&
            (s->left->color == BLACK) &&
            (s->right->color == BLACK)) {
                s->color = RED;
                delete_case1(n->parent);
        } else
                delete_case4(n);
}

情況4: 兄弟節點S 是黑色的、S 的兒子也都是黑色的,但是 N 的父親P,是紅色。
[還是對應我第二篇文章中,情況2:x的兄弟w是黑色的,且w的倆個孩子都是黑色的
(這裡,父節點P為紅)]
void delete_case4(struct node *n)
{
        struct node *s = sibling(n);
 
        if ((n->parent->color == RED) &&
            (s->color == BLACK) &&
            (s->left->color == BLACK) &&
            (s->right->color == BLACK)) {
                s->color = RED;
                n->parent->color = BLACK;
        } else
                delete_case5(n);
}

情況5: 兄弟S為黑色,S 的左兒子是紅色,S 的右兒子是黑色,而N是它父親的左兒子。
//此種情況,最後轉化到下面的情況6。
[對應我第二篇文章中,情況3:x的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。]
void delete_case5(struct node *n)
{
        struct node *s = sibling(n);
 
        if  (s->color == BLACK) 
                if ((n == n->parent->left) &&
                    (s->right->color == BLACK) &&
                    (s->left->color == RED)) { 
                        // this last test is trivial too due to cases 2-4.
                        s->color = RED;
                        s->left->color = BLACK;
                        rotate_right(s);
                } else if ((n == n->parent->right) &&
                           (s->left->color == BLACK) &&
                           (s->right->color == RED)) {
                       // this last test is trivial too due to cases 2-4.
                        s->color = RED;
                        s->right->color = BLACK;
                        rotate_left(s);
                }
        }
        delete_case6(n);  //轉到情況6。

情況6: 兄弟節點S是黑色,S的右兒子是紅色,而 N 是它父親的左兒子。
[對應我第二篇文章中,情況4:x的兄弟w是黑色的,且w的右孩子時紅色的。]
void delete_case6(struct node *n)
{
        struct node *s = sibling(n);
 
        s->color = n->parent->color;
        n->parent->color = BLACK;
 
        if (n == n->parent->left) {
                s->right->color = BLACK;
                rotate_left(n->parent);
        } else {
                s->left->color = BLACK;
                rotate_right(n->parent);
        }
}

//呵呵,畫這12張圖,直接從中午畫到了晚上。希望,此文能讓你明白。

四、紅黑樹的插入、刪除情況時間複雜度的分析
因為每一個紅黑樹也是一個特化的二叉查詢樹,
因此紅黑樹上的只讀操作與普通二叉查詢樹上的只讀操作相同。
然而,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。

恢復紅黑樹的屬性需要少量(O(log n))的顏色變更(實際是非常快速的)和
不超過三次樹旋轉(對於插入操作是兩次)。
雖然插入和刪除很複雜,但操作時間仍可以保持為 O(log n) 次。


ok,完。

後記:
此紅黑樹系列,前前後後,已經寫了4篇文章,如果讀者讀完了這4篇文章,
對紅黑樹有一個相對之前來說,比較透徹的理解,
那麼,也不枉費,我花這麼多篇幅、花好幾個鐘頭去畫紅黑樹了。

真正理解一個資料結構、演算法,最緊要的還是真正待用、實踐的時候體會。
歡迎,各位,將現在、或以後學習、工作中運用此紅黑樹結構、演算法的經驗與我分享。
謝謝。:D。
----------------------------------------