紅黑樹的實現原理

前一段在<<stl源碼剖析>>看到了紅黑樹,本來打算自己寫一篇理解的,但是實在沒有人家寫的清晰易懂,這裏轉載過來給大家看看吧。

比那個排名挺高的算法分析的透徹很多,反正csdn上那個我沒有看懂,以其昏昏使人昭昭。

原文地址:http://note.youdao.com/share/?id=ffe7439c125ece767b58c813c3d03b20&type=note

注:本文所有內容均翻譯自維基百科,部分內容爲原創。

強烈建議閱讀文章末尾的參考資料。

一、概述


紅黑樹是自平衡的二叉搜索樹,是計算機科學中的一種數據結構。

平衡是指所有葉子的深度基本相同(完全相等的情況並不多見,所以只能趨向於相等) 。

二叉搜索樹是指,節點最多有兩個兒子,且左子樹中所有節點都小於右子樹。

樹中節點有改動時,通過調整節點順序(旋轉),重新給節點染色,使節點滿足某種特殊的性質來保持平衡。

旋轉和染色過程肯定經過特殊設計可以高效的完成。

它不是完全平衡的二叉樹,但能保證搜索操作在O(log n)的時間複雜度內完成(n是樹中節點總數)。

插入、刪除以及旋轉、染色操作都是O(log n)的時間複雜度。

每個節點只需要用一位(bit)保存顏色(僅爲紅、黑兩種)屬性,除此以外,紅黑樹不需要保存其他信息,

所以紅黑樹與普通二叉搜索樹(BST)的內存開銷基本一樣,不會佔用太多內存。

二、歷史


The original data structure was invented in 1972 by Rudolf Bayer[2] and named "symmetric binary B-tree," but acquired its modern name in a paper in 1978 byLeonidas J. Guibas and Robert Sedgewickentitled "A Dichromatic Framework for Balanced Trees".[3] The color "red" was chosen because it was the best-looking color produced by the color laser printer available to the authors while working atXerox PARC.[4]

三、術語


A red–black tree is a special type of binary tree, used in computer science to organize pieces of comparable data, such as text fragments or numbers.

The leaf nodes of red–black trees do not contain data. These leaves need not be explicit in computer memory—a null child pointer can encode the fact that this child is a leaf—but it simplifies some algorithms for operating on red–black trees if the leaves really are explicit nodes. To save memory, sometimes a singlesentinel node performs the role of all leaf nodes; all references from internal nodes to leaf nodes then point to the sentinel node.

Red–black trees, like all binary search trees, allow efficient in-order traversal (that is: in the order Left–Root–Right) of their elements. The search-time results from the traversal from root to leaf, and therefore a balanced tree of n nodes, having the least possible tree height, results in O(log n) search time.

四、性質


bubuko.com,布布扣

上圖是一棵普通的紅黑樹

除了二叉樹的基本要求外,紅黑樹必須滿足以下幾點性質。

  1. 節點必須是紅色或者黑色。
  2. 根節點必須是黑色。
  3. 葉節點(NIL)是黑色的。(NIL節點無數據,是空節點)
  4. 紅色節點必須有兩個黑色兒子節點。
  5. 從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的。

這些約束使紅黑樹具有這樣一個關鍵屬性:從根節點到最遠的葉子節點的路徑長與到最近的葉子節點的路徑長度相差不會超過2。 因爲紅黑樹是近似平衡的。

另外,插入、刪除和查找操作與樹的高度成正比,所以紅黑樹的最壞情況,效率仍然很高。(不像普通的二叉搜索樹那麼慢)

解釋一下爲什麼有這樣好的效果。注意性質4和性質5。假設一個紅黑樹T,其到葉節點的最短路徑肯定全部是黑色節點(共B個),最長路徑肯定有相同個黑色節點(性質5:黑色節點的數量是相等),另外會多幾個紅色節點。性質4(紅色節點必須有兩個黑色兒子節點)能保證不會再現兩個連續的紅色節點。所以最長的路徑長度應該是2B個節點,其中B個紅色,B個黑色。

最短的路徑中全部是黑色節點,最長的路徑中既有黑色又有紅色節點。

因爲這兩個路徑中黑色節點個數是一樣的,而且不會出現兩個連續的紅色節點,所以最長的路徑可能會出現紅黑相間的節點。也就是說,樹中任意兩條路徑中的節點數相差不會超過一倍。

比如下圖:

bubuko.com,布布扣

五、類比四階的B樹


bubuko.com,布布扣

將之前的紅黑樹看成B樹,就是現在這個樣子。

紅黑樹可以看作是,每個節點簇包含1到3個關鍵值(Key)的四階B樹,所以就有2到4個子節點的指針。

這個B樹中每個節點簇中包含左鍵(LeftKey)、 中鍵(MidKey)、右鍵(RightKey),中鍵(MidKey)與紅黑樹中的黑色節點對應,另外兩個左右鍵(LeftKey,RightKey)與紅黑樹中的紅色節點對應。

還可以把此圖看成是紅色節點向上移動一個高度的紅黑樹。所以紅色節點就與它黑色的父親節點平行,組成一個 B樹節點簇。

這時,會發現在B樹中,所有紅黑樹的葉子節點都神奇的達到了相同的高度。

紅黑樹的結構與4階B樹(最少1個Key,最多3個Key的B樹)是相同的。

4階B樹與紅黑樹的對應轉換關係(圖片引用自LLRB):

  • B樹的節點簇有一個Key值,包含兩個子節點指針;對應紅黑樹中的一個黑色節點。

bubuko.com,布布扣

  • B樹的節點簇有三個Key值,包含四個子節點指針;中鍵對應紅黑樹中的黑色節點,左右鍵爲中鍵的紅色子節點。

bubuko.com,布布扣

  • B樹的節點簇有三個Key值,包含四個子節點指針;中鍵對應紅黑樹中的黑色節點,左右鍵爲中鍵的紅色子節點。

bubuko.com,布布扣

通過4階B樹可以很容易理解紅黑樹的插入、刪除操作。

任一點插入B樹,插入點肯定落在葉子節點簇上。如果節點簇有空間,那麼插入完成;如果沒有空間,則從當前節點簇中選出一個空閒的鍵值,將其放入父節點簇中。

從B樹中刪除任一點的問題,可以只考慮刪除最大鍵值或者刪除最小鍵值的情況。原因可以參考二叉搜索樹的刪除操作。

所以刪除時,刪除點也會落在葉子節點簇上。如果節點簇還有剩餘鍵值,那麼刪除完成;如果節點簇沒有剩餘節點,則從其父節點簇中選出任一鍵值補充至當前節點簇。然後在父節點遞歸進行刪除操作。

簡單來說,刪除或插入節點時,所做的調整操作都是爲了保持4階B樹的總體高度是一致的。

五、操作


紅黑樹的查找操作與二叉搜索樹BST完全一致。但是插入和刪除算法會破壞紅黑樹的性質。所以對紅黑樹執行刪除、插入操作後需要調整使其恢復紅黑樹性質。調整過程僅需要少量的染色(O(log n) 或者 O(1)的複雜度)和至多3次的旋轉操作(插入僅需2次)。雖然這樣會使插入、刪除操作很複雜,但其時間複雜度仍然在O(log n)以內。

建議不看下文描述的情況下,先在自己腦海中思考一下插入、刪除操作後,如何調整樹節點使其保持平衡狀態(對應4階B樹的形狀進行調整)。

有了自己的想法後,再對照文章的描述,會有更清晰的理解。

圖示左旋(Left rotation)右旋(Rgith rotation)

bubuko.com,布布扣

1.插入

插入操作與二叉搜索樹一樣,新節點肯定會作爲樹中的葉子節點的兒子加入(詳見二叉搜索樹相關說明),不過爲了恢復紅黑樹性質,還需要做些染色、旋轉等調整操作。另外需要注意的是,紅黑樹葉子節點是黑色的NIL節點,所以一般用帶有兩個黑色NIL兒子的新節點直接替換原先的NIL葉子節點,爲了方便後續的調整操作,新節點都是默認的紅色。

注:插入節點後的調整操作,主要目的是保證樹的總體高度不發生改變(使插入點爲紅色進入樹中);如果一定要改變樹的高度(插入點無法調整爲紅色),那麼所有操作的目的是使樹的整體高度增長1個單位,而不是僅某一子樹增長1個高度。

具體如何進行調整要看新節點周圍的節點顏色進行處理。下面是需要注意的幾種情況:

  • 性質3(所有的葉子節點都是黑色)不會被破壞,因爲葉子節點全部是黑色的NIL。
  • 性質4(紅色節點的兩個兒子必須是黑色)僅在添加一個紅色節點時,將黑色節點染成紅色時,或者進行旋轉操作時發生改變。
  • 性質5(從任一節點出發到葉子節點的路徑中黑色節點的數量相等)僅在添加黑色節點時,將紅色節點染成黑色時,或者進行旋轉操作時發生改變。

注意:我們使用New表示當前新插入的紅色節點,Parent表示N的父親節點,Grandparent表示N的爺爺節點,Uncle表示N的叔叔節點。另外,插入過程會發生遞歸循環(見case3),所以剛纔定義的節點角色並不會絕對固定於某一點,會根據情況(case)進行交換,但每個情況(case)的調整過程,角色肯定保持不變。

後面的圖示說明中,節點的顏色都與具體case相關。三角形一般表示未知深度的子樹。頂部帶有一個小黑點的三角形表示子樹的根是黑色,否則子樹的根是不確定的顏色。

每種case都使用C語言代碼展示。使用下面的節點獲取叔叔節點與爺爺節點。

struct node *grandparent(struct node *n)
{
 if ((n != NULL) && (n->parent != NULL))
  return n->parent->parent;
 else
  return NULL;
}struct node *uncle(struct node *n)
{
 struct node *g = grandparent(n);
 if (g == NULL)
  return NULL; // No grandparent means no uncle
 if (n->parent == g->left)
  return g->right;
 else
  return g->left;
}

Case1:當前節點N是樹中的根節點的情況。這時,將節點直接染成黑色以滿足性質2(根節點是黑色)。

由於N是根節點,所以這樣肯定也不會破壞性質5(從任一節點出發到葉子節點的路徑中黑色節點的數量相等)。

void insert_case1(struct node *n)
{
 if (n->parent == NULL)
  n->color = BLACK;
 else
  insert_case2(n);
}
 

Case2:當前節點的父親P是黑色的情況。這時,性質4(紅色節點必須有兩個黑色兒子節點)不會被破壞。性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)也仍然滿足,因爲節點N是紅色,但N還有兩個黑色的葉子節點NIL,所有通過N的路徑上,仍然保持和原來相同的黑色節點個數。

void insert_case2(struct node *n)
{
 if (n->parent->color == BLACK)
  return; /* Tree is still valid */
 else
  insert_case3(n);
}
 

Case3:當前節點的父親P和叔叔U都是紅色的情況。這時,將P、U都染成黑色,而G染成紅色以滿足性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)。現在,當前的紅色節點N有一個黑色的父親,而且所有經過父親和叔叔節點的路徑仍然保持與原來相同的節點個數。但是爺爺節點G可能會違反性質2(根節點必須是黑色)或者性質4(紅色節點必須有兩個黑色兒子節點)(在G節點的父親也是紅色節點時,會破壞性質4)。要修復這個問題,可以對節點G遞歸執行Case1的操作(可以這樣理解,把G當作是新插入的紅色節點,對G執行調整操作。因爲G的兩個子樹是平衡的)。這裏是尾遞歸調用,所以也可以使用循環的方法實現。因爲這之後肯定會執行一次旋轉操作,而且肯定提常數級的旋轉次數。

注:因爲P是紅色的,所以N肯定還有一個爺爺節點G。如果N沒有爺爺節點,那P節點就是根節點,應該是黑色纔對。由此可見,N還會有一個叔叔節點U,但U也可能是葉子節點(NIL),具體情況見Case4和Case5

bubuko.com,布布扣

void insert_case3(struct node *n)
{
 struct node *u = uncle(n), *g;
 
 if ((u != NULL) && (u->color == RED)) {
  n->parent->color = BLACK;
  u->color = BLACK;
  g = grandparent(n);
  g->color = RED;
  insert_case1(g);
 } else {
  insert_case4(n);
 }
}
 

Case4:父親P是紅色,叔叔U是黑色,並且N是P的右孩子,P是G的左孩子的情況。

這時,對節點P執行左旋操作,使P變成N的左孩子,N變成G的左孩子,也就是說進入了Case5 的情況。

旋轉操作完成之後,性質4(紅色節點必須有兩個黑色兒子節點)仍然不滿足。而性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)是仍然保持的,因爲旋轉操作使節點G出發到子樹1的路徑上多了一個節點N,G到子樹2的路徑上多了一個節點P,G到子樹3的路徑上少了一個節點P,而且P、N是紅色,不會影響路徑中黑色節點的數量。

由於旋轉操作後,性質4(紅色節點必須有兩個黑色兒子節點)仍然不滿足,所以我們直接進入Case5處理。

注:

Case4的主要目的就是將當前情況轉換到Case5進行處理。

Case4的說明和圖示中,我們僅提到了N是右孩子,P是左孩子的情況;另外N是左孩子,P是右孩子的情況沒有說明。因爲這兩種情況處理方法是相似的。不過在C代碼中包括了兩種情況的處理。

bubuko.com,布布扣

void insert_case4(struct node *n)
{
 struct node *g = grandparent(n);
 
 if ((n == n->parent->right) && (n->parent == g->left)) {
  rotate_left(n->parent);
 
 /*
 * rotate_left can be the below because of already having *g =  grandparent(n) 
 *
 * struct node *saved_p=g->left, *saved_left_n=n->left;
 * g->left=n; 
 * n->left=saved_p;
 * saved_p->right=saved_left_n;
 * 
 * and modify the parent‘s nodes properly
 */
 
  n = n->left; 
 
 } else if ((n == n->parent->left) && (n->parent == g->right)) {
  rotate_right(n->parent);
 
 /*
 * rotate_right can be the below to take advantage of already having *g =  grandparent(n) 
 *
 * struct node *saved_p=g->right, *saved_right_n=n->right;
 * g->right=n; 
 * n->right=saved_p;
 * saved_p->left=saved_right_n;
 * 
 */
 
  n = n->right; 
 }
 insert_case5(n);
}
 

Case5:父親P是紅色,但叔叔U是黑色, N是左孩子,P也是左孩子的情況。

此時,對節點G執行一次右旋。使P成爲N和G的父節點。已知G是黑色(P是紅色,爲了不破壞性質4(紅色節點必須有兩個黑色兒子節點),G肯定是黑色),所以將G染成紅色,P染成黑色。此時,既滿足性質4(紅色節點必須有兩個黑色兒子節點),也滿足性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)。唯一的改變是原來經過G節點的路徑,現在全部都會經過P節點。

bubuko.com,布布扣

void insert_case5(struct node *n)
{
 struct node *g = grandparent(n);
 
 n->parent->color = BLACK;
 g->color = RED;
 if (n == n->parent->left)
  rotate_right(g);
 else
  rotate_left(g);
}

注:到此爲止,插入操作的調整都結束了。

2.刪除

注:理解刪除操作的重點是,黑色節點刪除後,兒子節點中有紅色的則從兒子樹中選一節點填補被刪除後的空缺;否則,從兄弟子樹中選擇一個節點填補空缺;再否則,就將問題遞歸到父親節點處理。跟繼承皇位的辦法相似

在普通二叉搜索樹中刪除一個含有兩個非葉子兒子的節點時,我們會先找到此節點左子樹中最大的節點(也叫前驅),或者右子樹中的最小節點(也叫後繼),將找到的節點值替換到當前被刪除節點的位置,然後刪除前驅或者後繼節點(詳見這裏)。這裏被刪除的節點,至多有一個非葉子節點。因爲替換節點值的操作不會破壞紅黑樹的性質,所以刪除紅黑樹任一節點的問題就簡化爲,刪除一個含有至多一個非葉子兒子的情況。

後面的討論過程中,我們將這個被刪除的節點(含至多一個非葉子兒子)標記爲M。M唯一的一個非葉子兒子我們稱之爲C,如果M的兒子都是葉子節點,那麼兩個葉子都可稱爲C,不做區分。

如果M是紅色節點,只要用兒子C直接替換到M的位置即可(這僅發生在M有兩個葉子節點的情況,因爲假設M有一個黑色的兒子CL,CL不是葉子節點,所以CL還有兩個黑色的葉子CLL、CLR,M的另外一個兒子是葉子節點CR。那麼M節點違反性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的),所以C是黑色時肯定是葉子)。因爲原來經過被刪除的紅色節點的所有路徑中,僅少了一個紅色節點,且M的父親和兒子肯定是黑色,所以性質3(葉節點(NIL)是黑色的)和性質4(紅色節點必須有兩個黑色兒子節點)和性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)不受影響,

還有一種簡單的情況是,M是黑色,C是紅色時,如果只是用C替換到M的位置,可能會破壞性質4(紅色節點必須有兩個黑色兒子節點)和性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)。所以,只要再把C染成黑色,那麼原有性質全部不受影響。

比較複雜的情況是M和C都是黑色時(這僅發生在M有兩個葉子的情況,具體原因上一段已經說明)。仍然將C節點直接替換到M的位置,不過我們將位置新位置的C稱爲N,N的兄弟節點(以前是M的兄弟)稱爲Sibling。在下面的圖示中,我們會使用P表示N的父親(以前是M的父親),SL表示S的左兒子,SR表示S的右兒子(S肯定不是葉子節點,因爲M和C是黑色,所以P的兒子節點中,M所在子樹高度爲2,所以S所在子樹高度也是2,所以S肯定不是葉子)。

注:下面各種情況中,我們可能交換(改變)各個節點的角色。但在每種情況處理中角色名稱是固定不變的。

圖示中的節點不會覆蓋所有可能的顏色,只是爲了方便描述任舉一例。白色節點表示未知的顏色(可能是紅色也可能是黑色) 。

使用此函數獲取兄弟節點

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

Note: In order that the tree remains well-defined, we need that every null leaf remains a leaf after all transformations (that it will not have any children). If the node we are deleting has a non-leaf (non-null) child N, it is easy to see that the property is satisfied. If, on the other hand, N would be a null leaf, it can be verified from the diagrams (or code) for all the cases that the property is satisfied as well.

下面的代碼用來處理剛纔說的幾種簡單情況。函數replace_node()將節點child替換到節點n的位置(替換值,而不改變顏色)。另外,爲了操作方便,下面的代碼中使用一個真實的Node表示葉子節點(不是用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);
}


Note
: If N is a null leaf and we do not want to represent null leaves as actual node objects, we can modify the algorithm by first calling delete_case1() on its parent (the node that we delete, n in the code above) and deleting it afterwards. We can do this because the parent is black, so it behaves in the same way as a null leaf (and is sometimes called a ‘phantom‘ leaf). And we can safely delete it at the end as n will remain a leaf after all operations, as shown above.

如果N和它原來的父親(M)都是黑色,那麼刪除操作會使所有經過N節點的路徑都缺少一個黑色節點。因爲性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)被破壞,樹需要進行調整以保持平衡。下面詳細說一下需要考慮的幾種情況。

Case1:N是根節點。此時什麼也不需要做。因爲每條路徑都少了一個黑色節點,而且根是黑色的,所以所有性質都沒有被破壞。

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

注:在情況2、5、6中,我們都假定N是P的左兒子。對於N是右兒子的情況,處理方法也不復雜,只要將左右對調就行了。在示例代碼中允份考慮了這些情況。

Case2:S是紅色。

這時,我們交換P和S的顏色,然後對P執行左旋操作,使S成爲N的爺爺。注意,P節點肯定是黑色,因爲P的兒子S是紅色。此時,所有路徑上的黑色節點個數沒有變化,而N節點現在的兄弟SL變成了黑色,N節點現在的父親P變成了紅色。接下來,我們可以交給Case4、5、6繼續處理。在下面的情況中,我們會將N的新兄弟SL仍然稱做S。

bubuko.com,布布扣

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

Case3:P、S和S的兒子都是黑色的情況。

直接將S染成紅色。這樣恰好使經過S點的路徑上也少一個黑色節點,而經過N節點的路徑由於之前的刪除操作,現在也是少一個黑色節點的狀態。順其自然的,S、N是P的兒子,所以現在經過P點的路徑相比原來少了一個黑色節點。這麼做相當於,把原先存在於N節點的不平衡狀態上移到了P節點,現在P節點不滿足性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)。我們可以將P點交給Case1處理,這樣就形成一個遞歸。

bubuko.com,布布扣

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

Case4:S和S的兒子是黑色,但P是紅色的情況。

此時,只要把P和S點的顏色互換一下,即可使樹恢復平衡狀態。原先經過S點的路徑,黑色節點數量仍然保持不變;而原先經過N點的路徑,現在多了一個黑色節點P,正好彌補了刪除節點M後缺少一個黑色節點的問題。

bubuko.com,布布扣

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

Case5:P點顏色任意,S點是黑色,S左兒子是紅色,右兒子是黑色。N是P的左兒子(S是P的右兒子)的情況。

此時,我們對S點執行右旋轉,使得S的左兒子SL,既是S的父親,也是N的兄弟。同時交換S和SL的顏色,這樣所有路徑中黑色節點的數量沒有變化。

現在N點的兄弟節點S就有了一個紅色的右兒子,因爲我們可以直接進入Case6處理。

這次轉換對於P和N點沒有什麼影響。(需要再次說明的是,Case6中,我們把N的新兄弟仍然稱爲S)

bubuko.com,布布扣

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

Case6:P點顏色任意,S點是黑色,S的右兒子是紅色。N是P的左兒子(S是P的右兒子)的情況。

此時,我們對P點執行左旋轉,使S成爲P的父親(同時還是SR的父親)。

同時,交換P、S的顏色,並將SR染成黑色。此時S節點的左右子樹恢復了平衡,且與刪除節點M前有相同的黑色節點數。性質4(紅色節點必須有兩個黑色兒子節點)和性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)也已經恢復正常。

無論P節點先前是什麼顏色,N都比之前多了一個黑色祖先。假設P先前是紅色,現在P被染成了黑色;假設P先前就是黑色,現在P又多了一樣黑色的父節點S,所以經過N的路徑中,增加了一個黑色節點。

同時,還要說明一下不經過N點的路徑的變化情況,一共有兩種可能:

  • 經過N的新兄弟SL(即圖中標爲3的子樹)的路徑。無論是調整前,還是調整後,經過SL點的路徑都會經過S點和P點。因爲調整操作僅僅是將P、S互換的顏色和位置,所以這條路徑中黑色節點的數量沒有變化。
  • 經過N的新叔叔SR(S的右兒子,現在爲黑色)的路徑。調整前,此路徑會經過紅色的SR、黑色的S及顏色未知的P(取S的父親)。調整後,此路徑僅經過黑色的SR(由紅色染成黑色)、顏色未知的S(與P互換顏色)。因爲路徑長度僅計算黑色節點,所以這條路徑中黑色節點數量沒有變化。

綜上,這些路徑中黑色節點數量都沒有改變。因爲,我們修復了性質4(紅色節點必須有兩個黑色兒子節點)和性質5(從任一節點出發到其每個葉子節點的路徑,黑色節點的數量是相等的)。圖示中的白色節點可以是任意顏色(紅或黑),只要調整前後保持一致即可。

bubuko.com,布布扣

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

需要強調的是,這裏的函數使用的是尾部遞歸,所以算法是原地算法。上面的算法中,除了刪除算法的Case3以外,所有case都是按次序執行。this is the only case where an in-place implementation will effectively loop (after only one rotation in case 3).

另外,尾遞歸不會發生在兒子節點,一般都是從兒子節點向上,也就是向父親節點遞歸。而且遞歸次數不會超過O(log n)次(n是刪除節點前的節點總數)。只要在Case2中發生旋轉操作(這也是Case1到Case2間的循環過程中唯一可能發生的旋轉),N的父親就會變成紅色,所以循環會立即停止。因此循環過程最多發生一次旋轉。退出循環後最多發生兩次旋轉(Case5、Case6中)。也就是說,紅黑樹的刪除操作,總共不會超過3次旋轉。

注:更深入的“漸進邊界的證明”等其他內容這裏就略過不譯了。

六、參考


老外寫的左傾紅黑樹,是一種比本文所述的更好理解,且效率更高的紅黑樹。2008年發表。

http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf

維基百科原文(強烈建議看英文版)

http://en.wikipedia.org/wiki/Red%E2%80%93black_tree

看得見的紅黑樹(對紅黑樹的調整過程有不理解的地方,用此網址模擬一遍插入刪除操作,會讓思路更清晰)

https://www.cs.usfca.edu/~galles/visualization/RedBlack.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章