天天看點

查找、檢索 算法-總結4 紅黑樹 [RBT]

源位址: http://hxraid.javaeye.com/blog/611816

紅黑樹的性質與定義

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

1. 每一個結點要麼是紅色,要麼是黑色。

2. 根結點是黑色的。

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

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

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

查找、檢索 算法-總結4 紅黑樹 [RBT]

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

内部結點 —— 紅黑樹的非終結點

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

紅黑樹相關定理

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示例代碼來展示。通過下列函數,可以找到一個節點的叔父和祖父節點:  

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為紅色(原因在前面提到了)。

C代碼  void insert_case1(node n) {

if (n->parent == NULL)

n->color = BLACK;

else

insert_case2(n); //插入情況2

}  

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

       在這種情況下,我們插入一個紅色結點N(滿足性質5)。

Java代碼  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在進行各種情況的重新檢查。

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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。

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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。在各自的情形下,這都是三個節點中唯一的黑色節點。

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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的右兒子。我們将使用下述 函數找到兄弟節點:

C代碼  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 的實際節點對象來表示(在插入章節中的代碼可以同任何一種表示一起工作)。

C代碼  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 是新的根。

        在這種情況下,我們就做完了。我們從所有路徑去除了一個黑色節點,而新根是黑色的,是以屬性都保持着。

C代碼  void delete_case1(struct node *n)

{

if (n->parent != NULL)

delete_case2(n);

}

注意: 在情況2、5和6下,我們假定 N 是它父親的左兒子。如果它是右兒子,則在這些情況下的左和右應當對調。

情況2. S 是紅色。

        在這種情況下我們在N的父親上做左旋轉,把紅色兄弟轉換成N的祖父。我們接着對調 N 的父親和祖父的顔色。盡管所有的路徑仍然有相同數目的黑色節點,現在 N 有了一個黑色的兄弟和一個紅色的父親,是以我們可以接下去按 4、5或6情況來處理。(它的新兄弟是黑色因為它是紅色S的一個兒子。)

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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 上做重新平衡處理。

查找、檢索 算法-總結4 紅黑樹 [RBT]

 、

C代碼  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 的路徑上對黑色節點數目增加了一,添補了在這些路徑上删除的黑色節點。

查找、檢索 算法-總結4 紅黑樹 [RBT]

Java代碼  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 和它的父親都不受這個變換的影響。

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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。在示意圖中的白色節點可以是紅色或黑色,但是在變換前後都必須指定相同的顔色。

查找、檢索 算法-總結4 紅黑樹 [RBT]

C代碼  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++)等。