注:本文全部內容均翻譯自維基百科,部份內容爲原創。html
強烈建議閱讀文章末尾的參考資料。node
紅黑樹是自平衡的二叉搜索樹,是計算機科學中的一種數據結構。git
平衡是指全部葉子的深度基本相同(徹底相等的狀況並很少見,因此只能趨向於相等) 。算法
二叉搜索樹是指,節點最多有兩個兒子,且左子樹中全部節點都小於右子樹。數據結構
樹中節點有改動時,經過調整節點順序(旋轉),從新給節點染色,使節點知足某種特殊的性質來保持平衡。svg
旋轉和染色過程確定通過特殊設計能夠高效的完成。函數
它不是徹底平衡的二叉樹,但能保證搜索操做在O(log n)的時間複雜度內完成(n是樹中節點總數)。oop
插入、刪除以及旋轉、染色操做都是O(log n)的時間複雜度。ui
每一個節點只須要用一位(bit)保存顏色(僅爲紅、黑兩種)屬性,除此之外,紅黑樹不須要保存其餘信息,this
因此紅黑樹與普通二叉搜索樹(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 Sedgewick entitled "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 at Xerox 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.
上圖是一棵普通的紅黑樹
除了二叉樹的基本要求外,紅黑樹必須知足如下幾點性質。
這些約束使紅黑樹具備這樣一個關鍵屬性:從根節點到最遠的葉子節點的路徑長與到最近的葉子節點的路徑長度相差不會超過2。 由於紅黑樹是近似平衡的。
另外,插入、刪除和查找操做與樹的高度成正比,因此紅黑樹的最壞狀況,效率仍然很高。(不像普通的二叉搜索樹那麼慢)
解釋一下爲何有這樣好的效果。注意性質4和性質5。假設一個紅黑樹T,其到葉節點的最短路徑確定所有是黑色節點(共B個),最長路徑確定有相同個黑色節點(性質5:黑色節點的數量是相等),另外會多幾個紅色節點。性質4(紅色節點必須有兩個黑色兒子節點)能保證不會再現兩個連續的紅色節點。因此最長的路徑長度應該是2B個節點,其中B個紅色,B個黑色。
最短的路徑中所有是黑色節點,最長的路徑中既有黑色又有紅色節點。
由於這兩個路徑中黑色節點個數是同樣的,並且不會出現兩個連續的紅色節點,因此最長的路徑可能會出現紅黑相間的節點。也就是說,樹中任意兩條路徑中的節點數相差不會超過一倍。
好比下圖:
將以前的紅黑樹當作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):
經過4階B樹能夠很容易理解紅黑樹的插入、刪除操做。
任一點插入B樹,插入點確定落在葉子節點簇上。若是節點簇有空間,那麼插入完成;若是沒有空間,則從當前節點簇中選出一個空閒的鍵值,將其放入父節點簇中。
從B樹中刪除任一點的問題,能夠只考慮刪除最大鍵值或者刪除最小鍵值的狀況。緣由能夠參考二叉搜索樹的刪除操做。
因此刪除時,刪除點也會落在葉子節點簇上。若是節點簇還有剩餘鍵值,那麼刪除完成;若是節點簇沒有剩餘節點,則從其父節點簇中選出任一鍵值補充至當前節點簇。而後在父節點遞歸進行刪除操做。
簡單來講,刪除或插入節點時,所作的調整操做都是爲了保持4階B樹的整體高度是一致的。
紅黑樹的查找操做與二叉搜索樹BST徹底一致。可是插入和刪除算法會破壞紅黑樹的性質。因此對紅黑樹執行刪除、插入操做後須要調整使其恢復紅黑樹性質。調整過程僅須要少許的染色(O(log n) 或者 O(1)的複雜度)和至多3次的旋轉操做(插入僅需2次)。雖然這樣會使插入、刪除操做很複雜,但其時間複雜度仍然在O(log n)之內。
建議不看下文描述的狀況下,先在本身腦海中思考一下插入、刪除操做後,如何調整樹節點使其保持平衡狀態(對應4階B樹的形狀進行調整)。
有了本身的想法後,再對照文章的描述,會有更清晰的理解。
圖示左旋(Left rotation)右旋(Rgith rotation)
插入操做與二叉搜索樹同樣,新節點確定會做爲樹中的葉子節點的兒子加入(詳見二叉搜索樹相關說明),不過爲了恢復紅黑樹性質,還須要作些染色、旋轉等調整操做。另外須要注意的是,紅黑樹葉子節點是黑色的NIL節點,因此通常用帶有兩個黑色NIL兒子的新節點直接替換原先的NIL葉子節點,爲了方便後續的調整操做,新節點都是默認的紅色。
注:插入節點後的調整操做,主要目的是保證樹的整體高度不發生改變(使插入點爲紅色進入樹中);若是必定要改變樹的高度(插入點沒法調整爲紅色),那麼全部操做的目的是使樹的總體高度增加1個單位,而不是僅某一子樹增加1個高度。
具體如何進行調整要看新節點周圍的節點顏色進行處理。下面是須要注意的幾種狀況:
注意:咱們使用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
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代碼中包括了兩種狀況的處理。
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節點。
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); }
注:到此爲止,插入操做的調整都結束了。
注:理解刪除操做的重點是,黑色節點刪除後,兒子節點中有紅色的則從兒子樹中選一節點填補被刪除後的空缺;不然,從兄弟子樹中選擇一個節點填補空缺;再不然,就將問題遞歸到父親節點處理。跟繼承皇位的辦法類似
在普通二叉搜索樹中刪除一個含有兩個非葉子兒子的節點時,咱們會先找到此節點左子樹中最大的節點(也叫前驅),或者右子樹中的最小節點(也叫後繼),將找到的節點值替換到當前被刪除節點的位置,而後刪除前驅或者後繼節點(詳見這裏)。這裏被刪除的節點,至多有一個非葉子節點。由於替換節點值的操做不會破壞紅黑樹的性質,因此刪除紅黑樹任一節點的問題就簡化爲,刪除一個含有至多一個非葉子兒子的狀況。
後面的討論過程當中,咱們將這個被刪除的節點(含至多一個非葉子兒子)標記爲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); }
注:在狀況二、五、6中,咱們都假定N是P的左兒子。對於N是右兒子的狀況,處理方法也不復雜,只要將左右對調就好了。在示例代碼中允份考慮了這些狀況。
Case2:S是紅色。
這時,咱們交換P和S的顏色,而後對P執行左旋操做,使S成爲N的爺爺。注意,P節點確定是黑色,由於P的兒子S是紅色。此時,全部路徑上的黑色節點個數沒有變化,而N節點如今的兄弟SL變成了黑色,N節點如今的父親P變成了紅色。接下來,咱們能夠交給Case四、五、6繼續處理。在下面的狀況中,咱們會將N的新兄弟SL仍然稱作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); }
Case3:P、S和S的兒子都是黑色的狀況。
直接將S染成紅色。這樣剛好使通過S點的路徑上也少一個黑色節點,而通過N節點的路徑因爲以前的刪除操做,如今也是少一個黑色節點的狀態。順其天然的,S、N是P的兒子,因此如今通過P點的路徑相比原來少了一個黑色節點。這麼作至關於,把原先存在於N節點的不平衡狀態上移到了P節點,如今P節點不知足性質5(從任一節點出發到其每一個葉子節點的路徑,黑色節點的數量是相等的)。咱們能夠將P點交給Case1處理,這樣就造成一個遞歸。
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後缺乏一個黑色節點的問題。
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)
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點的路徑的變化狀況,一共有兩種可能:
綜上,這些路徑中黑色節點數量都沒有改變。由於,咱們修復了性質4(紅色節點必須有兩個黑色兒子節點)和性質5(從任一節點出發到其每一個葉子節點的路徑,黑色節點的數量是相等的)。圖示中的白色節點能夠是任意顏色(紅或黑),只要調整先後保持一致便可。
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的父親就會變成紅色,因此循環會當即中止。所以循環過程最多發生一次旋轉。退出循環後最多發生兩次旋轉(Case五、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