紅黑樹

紅黑樹的性質與定義node

紅黑樹(red-black tree) 是一棵知足下述性質的二叉查找樹:算法

1. 每個結點要麼是紅色,要麼是黑色。數據結構

2. 根結點是黑色的。函數

3. 全部葉子結點都是黑色的(實際上都是Null指針,下圖用NIL表示)。葉子結點不包含任何關鍵字信息,全部查詢關鍵字都在非終結點上。this

4. 每一個紅色結點的兩個子節點必須是黑色的。換句話說:從每一個葉子到根的全部路徑上不能有兩個連續的紅色結點spa

5. 從任一結點到其每一個葉子的全部路徑都包含相同數目的黑色結點3d

黑深度 ——從某個結點x出發(不包括結點x自己)到葉結點(包括葉子結點)的路徑上的黑結點個數,稱爲該結點x的黑深度,記爲bd(x),根結點的黑深度就是該紅黑樹的黑深度。葉子結點的黑深度爲0。好比:上圖bd(13)=2,bd(8)=2,bd(1)=1指針

內部結點 —— 紅黑樹的非終結點code

外部節點 —— 紅黑樹的葉子結點對象

 

紅黑樹相關定理

1. 從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長。

      根據上面的性質5咱們知道上圖的紅黑樹每條路徑上都是3個黑結點。所以最短路徑長度爲2(沒有紅結點的路徑)。再根據性質4(兩個紅結點不能相連)和性質1,2(葉子和根必須是黑結點)。那麼咱們能夠得出:一條具備3個黑結點的路徑上最多隻能有2個紅結點(紅黑間隔存在)。也就是說黑深度爲2(根結點也是黑色)的紅黑樹最長路徑爲4,最短路徑爲2。從這一點咱們能夠看出紅黑樹是 大體平衡的。 (固然比平衡二叉樹要差一些,AVL的平衡因子最多爲1)

2. 紅黑樹的樹高(h)不大於兩倍的紅黑樹的黑深度(bd),即h<=2bd

      根據定理1,咱們不難說明這一點。bd是紅黑樹的最短路徑長度。而可能的最長路徑長度(樹高的最大值)就是紅黑相間的路徑,等於2bd。所以h<=2bd。

3. 一棵擁有n個內部結點(不包括葉子結點)的紅黑樹的樹高h<=2log(n+1)

      下面咱們首先證實一顆有n個內部結點的紅黑樹知足n>=2^bd-1。這能夠用數學概括法證實,施概括於樹高h。當h=0時,這至關因而一個葉結點,黑高度bd爲0,而內部結點數量n爲0,此時0>=2^0-1成立。假設樹高h<=t時,n>=2^bd-1成立,咱們記一顆樹高 爲t+1的紅黑樹的根結點的左子樹的內部結點數量爲nl,右子樹的內部結點數量爲nr,記這兩顆子樹的黑高度爲bd'(注意這兩顆子樹的黑高度必然一 樣),顯然這兩顆子樹的樹高<=t,因而有nl>=2^bd'-1以及nr>=2^bd'-1,將這兩個不等式相加有nl+nr>=2^(bd'+1)-2,將該不等式左右加1,獲得n>=2^(bd'+1)-1,很顯然bd'+1>=bd,因而前面的不等式能夠 變爲n>=2^bd-1,這樣就證實了一顆有n個內部結點的紅黑樹知足n>=2^bd-1。

        在根據定理2,h<=2bd。即n>=2^(h/2)-1,那麼h<=2log(n+1)

        從這裏咱們可以看出,紅黑樹的查找長度最多不超過2log(n+1),所以其查找時間複雜度也是O(log N)級別的。

紅黑樹的操做

 

由於每個紅黑樹也是一個特化的二叉查找樹,所以紅黑樹上的查找操做與普通二叉查找樹上的查找操做相同。然而,在紅黑樹上進行插入操做和刪除操做會致使不 再符合紅黑樹的性質。恢復紅黑樹的屬性須要少許(O(log n))的顏色變動(實際是很是快速的)和不超過三次樹旋轉(對於插入操做是兩次)。 雖然插入和刪除很複雜,但操做時間仍能夠保持爲 O(log n) 次 。

 

插入操做

咱們首先以二叉查找樹的方法增長節點並標記它爲紅色。 ( 若是設爲黑色,就會致使根到葉子的路徑上有一條路上,多一個額外的黑節點,這個是很難調整的。可是設爲紅色節點後,可能會致使出現兩個連續紅色節點的衝突,那麼能夠經過顏色調換(color flips)和樹旋轉來調整。) 下面要進行什麼操做取決於其餘臨近節點的顏色。同人類的家族樹中同樣,咱們將使用術語叔父節點來指一個節點的父節點的兄弟節點。

 

假設新加入的結點爲N,父親結點爲P,叔父結點爲Ui(叔父結點就是一些列P的兄弟結點),祖父結點G(父親結點P的父親)。下面會給出每一種狀況,咱們將使用C示例代碼來展現。經過下列函數,能夠找到一個節點的叔父和祖父節點:  

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位於樹的根上,沒有父結點。

 

       此時很簡單,咱們將直接插入一個黑結點N(知足性質2),其餘狀況下插入的N爲紅色(緣由在前面提到了)。

 void insert_case1(node n) {
     if (n.parent == NULL)
         n.color = BLACK;
     else
         insert_case2(n); //插入狀況2
 }

狀況2. 新結點N的父結點P是黑色。

 

       在這種狀況下,咱們插入一個紅色結點N(知足性質5)。

 void insert_case2(node n) {
     if (n->parent->color == BLACK)
         return; // 樹仍舊有效
     else
         insert_case3(n); //插入狀況3
 }

注意:在狀況3,4,5下,咱們假定新節點有祖父節點,由於父節點是紅色;而且若是它是根,它就應當是黑色。因此新節點總有一個叔父節點,儘管在情形4和5下它多是葉子。

 

狀況3.若是父節點P和叔父節點U兩者都是紅色。

      以下圖,由於新加入的N結點必須爲紅色,那麼咱們能夠將父結點P(保證性質4),以及N的叔父結點U(保證性質5)從新繪製成黑色。若是此時祖父結點G是根,則結束變化。若是不是根,則祖父結點重繪爲紅色(保證性質5)。可是,G的父親也多是紅色的,爲了保證性質4。咱們把G遞歸當作新加入的結點N在進行各類狀況的從新檢查。

 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));
     }
     else
         insert_case4(n);
 }

注意:在情形4和5下,咱們假定父節點P 是祖父結點G 的左子節點。若是它是右子節點,情形4和情形5中的左和右應當對調。

 

狀況4. 父節點P是紅色而叔父節點U是黑色或缺乏; 另外,新節點N是其父節點P的右子節點,而父節點P又是祖父結點G的左子節點。

 

       以下圖, 在這種情形下,咱們進行一次左旋轉調換新節點和其父節點的角色(與AVL樹的左旋轉相同); 這致使某些路徑經過它們之前不經過的新節點N或父節點P中的一個,可是這兩個節點都是紅色的,因此性質5沒有失效。但目前狀況將違反性質4,因此接着,咱們按下面的狀況5繼續處理之前的父節點P。

 

 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. 父節點P是紅色而叔父節點U 是黑色或缺乏,新節點N 是其父節點的左子節點,而父節點P又是祖父結點的G的左子節點。

 

       以下圖: 在這種情形下,咱們進行鍼對祖父節點P 的一次右旋轉; 在旋轉產生的樹中,之前的父節點P如今是新節點N和之前的祖父節點G 的父節點。咱們知道之前的祖父節點G是黑色,不然父節點P就不多是紅色。咱們切換之前的父節點P和祖父節點G的顏色,結果的樹知足性質4[3]。性質 5[4]也仍然保持知足,由於經過這三個節點中任何一個的全部路徑之前都經過祖父節點G ,如今它們都經過之前的父節點P。在各自的情形下,這都是三個節點中惟一的黑色節點。

 

 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 {
         /* Here, n == n->parent->right && n->parent == grandparent(n)->right */
         rotate_left(grandparent(n));
     }
 }

刪除操做

 

若是須要刪除的節點有兩個兒子,那麼問題能夠被轉化成刪除另外一個只有一個兒子的節點的問題(爲了表述方便,這裏所指的兒子,爲非葉子節點的兒子)。 對於二叉查找樹,在刪除帶有兩個非葉子兒子的節點的時候,咱們找到要麼在它的左子樹中的最大元素、要麼在它的右子樹中的最小元素,並把它的值轉移到要刪除 的節點中(如在這裏所展現的那樣)。咱們接着刪除咱們從中複製出值的那個節點,它一定有少於兩個非葉子的兒子。由於只是複製了一個值而不違反任何屬性,這 就把問題簡化爲如何刪除最多有一個兒子的節點的問題。它不關心這個節點是最初要刪除的節點仍是咱們從中複製出值的那個節點。

 

在本文餘下的部分中,咱們只須要討論刪除只有一個兒子的節點(若是它兩個兒子都爲空,即均爲葉子,咱們任意將其中一個看做它的兒子)。若是咱們刪除一個紅色節點,它的父親和兒子必定是黑色的。因此咱們能夠簡單的用它的黑色兒子替換它,並不會破壞屬性3和4。經過被刪除節點的全部路徑只是少了一個紅色 節點,這樣能夠繼續保證屬性5。另外一種簡單狀況是在被刪除節點是黑色而它的兒子是紅色的時候。若是隻是去除這個黑色節點,用它的紅色兒子頂替上來的話,會 破壞屬性4,可是若是咱們重繪它的兒子爲黑色,則曾經經過它的全部路徑將經過它的黑色兒子,這樣能夠繼續保持屬性4。

 

須要進一步討論的是在要刪除的節點和它的兒子兩者都是黑色的時候,這是一種複雜的狀況。咱們首先把要刪除的節點替換爲它的兒子。出於方便,稱呼這個兒子爲 N,稱呼它的兄弟(它父親的另外一個兒子)爲S。在下面的示意圖中,咱們仍是使用P稱呼N的父親,SL稱呼S的左兒子,SR稱呼S的右兒子。咱們將使用下述 函數找到兄弟節點:

struct node * sibling(struct node *n)
{
        if (n == n->parent->left)
                return n->parent->right;
        else
                return n->parent->left;
}

 咱們可使用下列代碼進行上述的概要步驟,這裏的函數 replace_node 替換 child 到 n 在樹中的位置。出於方便,在本章節中的代碼將假定空葉子被用不是 NULL 的實際節點對象來表示(在插入章節中的代碼能夠同任何一種表示一塊兒工做)。

void delete_one_child(struct node *n)
{
        /*
         * Precondition: n has at most one non-null child.
         */
        struct node *child = is_leaf(n->right) ? n->left : n->right;
 
        replace_node(n, child);
        if (n->color == BLACK) {
                if (child->color == RED)
                        child->color = BLACK;
                else
                        delete_case1(child);
        }
        free(n);
}

若是 N 和它初始的父親是黑色,則刪除它的父親致使經過 N 的路徑都比不經過它的路徑少了一個黑色節點。由於這違反了屬性 4,樹須要被從新平衡。有幾種狀況須要考慮:

 

狀況1. N 是新的根。

        在這種狀況下,咱們就作完了。咱們從全部路徑去除了一個黑色節點,而新根是黑色的,因此屬性都保持着。

void delete_case1(struct node *n)
{
        if (n->parent != NULL)
                delete_case2(n);
}

注意: 在狀況二、5和6下,咱們假定 N 是它父親的左兒子。若是它是右兒子,則在這些狀況下的左和右應當對調。

 

狀況2. S 是紅色。

 

        在這種狀況下咱們在N的父親上作左旋轉,把紅色兄弟轉換成N的祖父。咱們接着對調 N 的父親和祖父的顏色。儘管全部的路徑仍然有相同數目的黑色節點,如今 N 有了一個黑色的兄弟和一個紅色的父親,因此咱們能夠接下去按 四、5或6狀況來處理。(它的新兄弟是黑色由於它是紅色S的一個兒子。)

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: N 的父親、S 和 S 的兒子都是黑色的。

 

       在這種狀況下,咱們簡單的重繪 S 爲紅色。結果是經過S的全部路徑, 它們就是之前不經過 N 的那些路徑,都少了一個黑色節點。由於刪除 N 的初始的父親使經過 N 的全部路徑少了一個黑色節點,這使事情都平衡了起來。可是,經過 P 的全部路徑如今比不經過 P 的路徑少了一個黑色節點,因此仍然違反屬性4。要修正這個問題,咱們要從狀況 1 開始,在 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 的父親是紅色。

 

       在這種狀況下,咱們簡單的交換 N 的兄弟和父親的顏色。這不影響不經過 N 的路徑的黑色節點的數目,可是它在經過 N 的路徑上對黑色節點數目增長了一,添補了在這些路徑上刪除的黑色節點。

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 是它父親的左兒子。

 

      在這種狀況下咱們在 S 上作右旋轉,這樣 S 的左兒子成爲 S 的父親和 N 的新兄弟。咱們接着交換 S 和它的新父親的顏色。全部路徑仍有一樣數目的黑色節點,可是如今 N 有了一個右兒子是紅色的黑色兄弟,因此咱們進入了狀況 6。N 和它的父親都不受這個變換的影響。

void delete_case5(struct node *n)
{
        struct node *s = sibling(n);
 
        if  (s->color == BLACK) 
/* this if statement is trivial,
due to Case 2 (even though Case two changed the sibling to a sibling's child,
the sibling's child can't be red, since no red parent can have a red child). */

// the following statements just force the red to be on the left of the left of the parent,
// or right of the right, so case six will rotate correctly.
                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. S 是黑色,S 的右兒子是紅色,而 N 是它父親的左兒子。

 

       在這種狀況下咱們在 N 的父親上作左旋轉,這樣 S 成爲 N 的父親和 S 的右兒子的父親。咱們接着交換 N 的父親和 S 的顏色,並使 S 的右兒子爲黑色。子樹在它的根上的還是一樣的顏色,因此屬性 3 沒有被違反。可是,N 如今增長了一個黑色祖先: 要麼 N 的父親變成黑色,要麼它是黑色而 S 被增長爲一個黑色祖父。因此,經過 N 的路徑都增長了一個黑色節點。

       此時,若是一個路徑不經過 N,則有兩種可能性:

      它經過 N 的新兄弟。那麼它之前和如今都一定經過 S 和 N 的父親,而它們只是交換了顏色。因此路徑保持了一樣數目的黑色節點。 
      它經過 N 的新叔父,S 的右兒子。那麼它之前經過 S、S 的父親和 S 的右兒子,可是如今只經過 S,它被假定爲它之前的父親的顏色,和 S 的右兒子,它被從紅色改變爲黑色。合成效果是這個路徑經過了一樣數目的黑色節點。 
      在任何狀況下,在這些路徑上的黑色節點數目都沒有改變。因此咱們恢復了屬性 4。在示意圖中的白色節點能夠是紅色或黑色,可是在變換先後都必須指定相同的顏色。

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);
        }
}

 一樣的,函數調用都使用了尾部遞歸,因此算法是就地的。此外,在旋轉以後再也不作遞歸調用,因此進行了恆定數目(最多 3 次)的旋轉。

紅黑樹的優點

 

紅黑樹可以以O(log2(N))的時間複雜度進行搜索、插入、刪除操做。此外,任何不平衡都會在3次旋轉以內解決。這一點是AVL所不具有的。

 

並且實際應用中,不少語言都實現了紅黑樹的數據結構。好比 TreeMap, TreeSet(Java )、 STL(C++)等。

相關文章
相關標籤/搜索