最近花了些時間重拾數據結構的基礎知識,先嚐試了紅黑樹,花了大半個月的時間研究其原理和實現,下面是學習到的知識和一些筆記的分享。望各位多多指教。本次代碼的實現請點擊:紅黑樹實現代碼 – gisthtml
紅黑樹是帶有 color 屬性的二叉搜索樹,color 的值爲紅色或黑色,所以叫作紅黑樹。node
對紅黑樹的每一個結點的結構體定義以下:git
1github 2算法 3數據結構 4函數 5學習 6網站 7spa 8 |
struct RBNode { int color; void *key; void *value; struct RBNode *left; struct RBNode *right; struct RBNode *parent; }; |
設根結點的 parent 指針指向 NULL,新結點的左右孩子 left 和 right 指向 NULL。葉子結點是 NULL。
定義判斷紅黑樹顏色的宏爲
1 |
#define ISRED(x) ((x) != NULL && (x)->color == RED) |
所以,葉子結點 NULL 的顏色爲非紅色,在紅黑樹中,它就是黑色,包括黑色的葉子結點。
黑高的定義,從某個結點 x 觸發(不含該結點)到達一個葉結點的任意一條簡單路徑上的黑色結點個數稱爲該結點的黑高(black-height),記做 bh(x)。
下面是一個紅黑樹的例子
旋轉操做在樹的數據結構裏面很常常出現,好比 AVL 樹,紅黑樹等等。不少人都瞭解旋轉的操做是怎麼進行的(HOW),在網上能找到不少資料描述旋轉的步驟,可是卻沒有人告訴我爲何要進行旋轉(WHY)?爲何要這樣旋轉?經過與朋友交流,對於紅黑樹來講,之因此要旋轉是由於左右子樹的高度不平衡,即左子樹比右子樹高或者右子樹比左子樹高。那麼,以左旋爲例,經過左旋轉,就能夠將左子樹的黑高 +1,同時右子樹的黑高 -1,從而恢復左右子樹黑高平衡。
以右旋爲例,α 和 β 爲 x 的左右孩子,γ 爲 y 的右孩子,由於 y 的左子樹比右子樹高度多一,所以以 y 爲根的子樹左右高度不平衡,那麼以 y-x 爲軸左旋使其左右高度平衡,左旋以後 y 和 β 同時成爲 x 的右孩子,然而由於要旋轉的是 x 和 y 結點,所以就讓 β 成爲 y 的左孩子便可。
旋轉的算法複雜度:從圖示可知,旋轉的操做只是作了修改指針的操做,所以算法複雜度是 O(1)。
紅黑樹的全部操做的算法複雜度都是 O(lgn)。這是由於紅黑樹的最大高度是 2lg(n+1)。
證實以下:
設每一個路徑的黑色節點的數量爲 bh(x),要證實紅黑樹的最大高度是 2lg(n+1),首先證實任何子樹包含 2^bh(x) - 1 個內部節點。
下面使用數學概括法證實。
當 bh(x) 等於 0 時,即有 0 個節點,那麼子樹包含 2^0 - 1 = 0 個內部節點,得證。
對於其餘節點,其黑高爲 bh(x) 或 bh(x) - 1,當 x 是紅節點時,黑高爲 bh(x),不然,爲 bh(x) - 1。對於下一個節點,由於每一個孩子節點都比父節點的高度低,所以概括假設每一個子節點至少有 2^bh(x)-1 - 1 個內部節點,所以,以 x 爲根的子樹至少有 2^(bh(x)-1) - 1 + 2^(bh(x)-1) - 1 = 2^bh(x) - 1個內部節點。
設 h 是樹高,根據性質 4 可知道,每一條路徑至少有一半的節點是黑的,所以 bh(x) - 1 = h/2。
那麼紅黑樹節點個數就爲 n >= 2^h/2 - 1。
可得 n + 1 >= 2^h/2。兩邊取對數得:
|
|
由上面的證實可得,紅黑樹的高度最大值是 2log(n+1),所以紅黑樹查找的複雜度爲 O(lgn)。對於紅黑樹的插入和刪除操做,算法複雜度也是 O(lgn),所以紅黑樹的全部操做都是 O(lgn)
的複雜度。
紅黑樹的插入操做,先找到要新節點插入的位置,將節點賦予紅色,而後插入新節點。最後作紅黑樹性質的修復。
由於插入操做只可能會違反性質 二、四、5,對於性質 2,只須要直接將根節點變黑便可;那麼須要處理的就有性質 4 和性質 5,若是插入的是黑節點,那麼就會影響新節點所在子樹的黑高,這樣一來就會違反性質 5,若是新節點是紅色,那麼新插入的節點就不會違反性質 5,只須要處理違反性質 2 或性質 4 的狀況。即根節點爲紅色或者存在兩個連續的紅節點。簡而言之,就是減小修復紅黑性質被破壞的狀況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
RB-INSERT(T, node) walk = T.root prev = NULL while (walk != NULL) prev = walk if (node.key < walk.key) walk = walk.left else walk = walk.right node.parent = walk if (walk == NULL) T.root = node else if (node.key < walk.key) walk.left = node else walk.right = node RB-INSERT-FIXUP(T, node) |
插入以後,若是新結點(node)的父結點(parent)或者根節點(root)是紅色,那麼就會違反了紅黑樹的性質 4 或性質 2。對於後者,只須要直接將 root 變黑便可。
而前者,違反了性質 4 的,即紅黑樹出現了連續兩個紅結點的狀況。修復的變化還要看父結點是祖父結點的左孩子仍是右孩子,左右兩種狀況是對稱的,此處看父結點是祖父結點的左孩子的狀況。要恢復紅黑樹的性質,那麼就須要將 parent 的其中一個變黑,這樣的話,該結點所在的子樹的黑高 +1,這樣就會破壞了性質 5,違背了初衷。所以須要將 parent->parent(grandparent)的另外一個結點(uncle 結點)的黑高也 +1 來維持紅黑樹的性質。
若是 uncle 是紅色,那麼直接將 uncle 變爲黑色,同時 parent 也變黑。可是這樣一來,以 grandparent 爲根所在的子樹的黑高就 +1,所以將 grandparent 變紅使其黑高減一,而後將 node 指向 grandparent,讓修復結點上升兩個 level,直到遇到根結點爲止。
若是 uncle 是黑色,那麼就不能將 uncle 變黑了。那麼只能將紅節點上升給祖父節點,即將祖父結點變紅,而後將父結點變黑,這樣一來,以父結點爲根的子樹的左右子樹就不平衡了,此時左子樹比右子樹的黑高多 1,那麼就須要經過將祖父結點右旋以調整左右平衡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
RB-INSERT-FIXUP(T, node) while IS_RED(node) parent = node->parent if !IS_RED(parent) break grandparent = parent->parent if parent == grandparent.left uncle = grandparent.right if IS_RED(uncle) parent.color = BLACK uncle.color = BLACK grandparent.color = RED node = grandparent elseif node == parent.right LEFT_ROTATE(T, parent) swap(node, parent) else parent.color = BLACK grandparent.color = RED RIGHT_ROTATE(T, grandparent) else same as then clause with "right" and "left" exchanged
T.root.color = BLACK |
插入的步驟主要有兩步
a. 找到新結點的插入位置 b. 進行插入修復。而插入修復包括旋轉和使修復結點上升。
對於 a,從上面可知,查找的算法複雜度是 O(lgn)。
對於 b,插入修復中,每一次修復結點上升 2 個 level,直到遇到根結點,走過的路徑最大值是樹的高度,算法複雜度是 O(lgn);由旋轉的描述可得其算法複雜度是 O(1),所以插入修復的算法複雜度是 O(lgn)。
綜上所述,插入的算法複雜度 O(INSERT) = O(lgn) + O(lgn) = O(lgn)。
紅黑樹的刪除操做,先找到要刪除的結點,而後找到要刪除結點的後繼,用其後繼替換要刪除的結點的位置,最後再作紅黑樹性質的修復。
紅黑樹的刪除操做比插入操做更復雜一些。
要刪除一個結點(node),首先要找到該結點所在的位置,接着,判斷 node 的子樹狀況。
- 若是 node 只有一個子樹,那麼將其後繼(successor)替換掉 node 便可;
- 若是 node 有兩個子樹,那麼就找到 node 的 successor 替換掉 node;
- 若是 successor 是 node 的右孩子,那麼直接將 successor 替換掉 node 便可,可是須要將 successor 的顏色變爲 node 的顏色;
- 若是 successor 不是 node 的右孩子,而由於 node 的後繼是沒有左孩子的(這個能夠查看相關證實),因此刪除掉 node 的後繼 successor 以後,須要將 successor 的右孩子 successor.right 補上 successor 的位置。
刪除過程當中須要保存 successor 的顏色 color,由於刪除操做可能會致使紅黑樹的性質被破壞,而刪除操做刪除的是 successor。所以,每一次改變 successor 的時候,都要更新 color。
TRANSPLANT(T, u, v) 是移植結點的操做,此函數的功能是使結點 v 替換結點 u 的位置。在刪除操做中用來將後繼結點替換到要刪除結點的位置。
用 x 表示有非空左右孩子的結點。在樹的中序遍歷中,在 x 的左子樹的結點在 x 的前面,在 x 的右子樹的結點都在 x 的後面。所以,x 的前驅在其左子數,後繼在其右子樹。
假設 s 是 x 的後繼。那麼 s 不能有左子樹,由於在中序遍歷中,s 的左子樹會在 x 和 s 的中間。(在 x 的後面是由於其在 x 的右子樹中,在 s 的前面是由於其在 x 的左子樹中。)在中序遍歷中,與前面的假設同樣,若是任何結點在 x 和 s 之間,那麼該結點就不是 x 的後繼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
RB-DELETE(T, node) color = node.color walk_node = node if IS_NULL(node.left) need_fixup_node = node.right transplant(T, node, need_fixup_node) elseif IS_NULL(node.right) need_fixup_node = node.left transplant(T, node, need_fixup_node) else walk_node = minimum(node.right) color = walk_node.color need_fixup_node = walk_node.right if walk_node.parent != node transplant(T, walk_node, walk_node.right) walk_node.right = node.right walk_node.right.parent = walk_node transplant(T, node, walk_node) walk_node.left = node.left walk_node.left.parent = walk_node walk_node.color = node.color
if color == BLACK RB-DELETE-FIXUP(T, need_fixup_node) |
注:筆者參考的是算法導論的僞代碼,可是在實現的時候,由於用 NULL 表示空結點,若是須要修復的結點 need_fixup_node爲空時沒法拿到其父結點,所以保存了其父結點 need_fixup_node_parent 及其所在方向 direction,爲刪除修復時訪問其父結點及其方向時作調整。
刪除過程當中須要保存 successor 的顏色 color,由於刪除操做可能會致使紅黑樹的性質被破壞,而刪除操做刪除的是 successor。所以,每一次改變 successor 的時候,都要更新 color。
會致使紅黑樹性質被破壞的狀況就是 successor 的顏色是黑色,當 successor 的顏色是紅色的時候,不會破壞紅黑樹性質,理由以下:
- 性質 1,刪除的是紅結點,不會改變其餘結點顏色,所以不會破壞。
- 性質 2,若是刪除的是紅結點,那麼該結點不多是根結點,所以根結點的性質不會被破壞。
- 性質 3,葉子結點的顏色保持不變。
- 性質 4,刪除的是紅結點,由於原來的樹是紅黑樹,因此不可能出現連續兩個結點爲紅色的狀況。由於刪除是 successor 只是替換 node 的位置,可是顏色被改成 node 的顏色。另外,若是 successor 不是node 的右孩子,那麼就須要先將 successor 的右孩子 successor->right 替換掉 successor,若是 successor 是紅色,那麼 successor->right 確定是黑色,所以也不會形成兩個連續紅結點的狀況。性質 4 不被破壞。
- 性質 5,刪除的是紅結點,不會影響黑高,所以性質 5 不被破壞。
若是刪除的是黑結點,可能破壞的性質是 二、四、5。理由及恢復方法以下:
- 若是 node 是黑,其孩子是紅,且 node 是 root,那麼就會違反性質 2;(修復此性質只須要將 root 直接變黑便可)
- 若是刪除後 successor 和 successor->right 都是紅,那麼會違反性質 4;(直接將 successor->right 變黑就能夠恢復性質)
- 若是黑結點被刪除,會致使路徑上的黑結點 -1,違反性質 5。
那麼剩下性質 5 較難恢復,不妨假設 successor->right 有一層額外黑色,那麼性質 5 就得以維持,而這樣作就會破壞了性質 1。由於此時 new_successor 就爲 double black(BB)或 red-black(RB)。那麼就須要修復new_successor 的顏色,將其「額外黑」上移,使其紅黑樹性質完整恢復。
注意:該假設只是加在 new_successor 的結點上,而不是該結點的顏色屬性。
若是是 R-B 狀況,那麼只須要將 new_successor 直接變黑,那麼「額外黑」就上移到 new_successor 了,修復結束。
若是是 BB 狀況,就須要將多餘的一層「額外黑」繼續上移。此處還要看 new_successor 是原父結點的左孩子仍是右孩子,這裏設其爲左孩子,左右孩子的狀況是對稱的。
若是直接將額外黑上移給父結點,那麼以 new_successor 的父結點爲根的子樹就會失去平衡,由於左子樹的黑高 -1 了。所以須要根據 new_successor 的兄弟結點 brother 的顏色來考慮調整。
若是 brother 是紅色,那麼 brother 的兩個孩子和 parent 都是黑色,此時額外黑就沒法上移給父結點了,那麼就須要作一些操做,將 brother 和 parent 的顏色交換,使得 brother 變黑, parent 變紅,這樣的話,brother 所在的子樹黑高就 +1 了,以 parent 爲根作一次左旋恢復黑高平衡。旋轉以後,parent 是紅色的,且 brother 的其中一個孩子成爲了 parent 的新的右孩子結點,將 brother 從新指向新的兄弟結點,而後接着考慮其餘狀況。
若是 brother 是黑色,那麼就須要經過將 brother 的黑色和 successor 的額外黑組成的一重黑色上移達到目的,而要上移 brother 的黑色,還須要考慮其孩子結點的顏色。
若是 brother->right 和 brother->right 都是黑色,那麼好辦,直接將黑色上移,即 brother->color = RED。此時包含額外黑的結點就變成了 parent。parent 爲 RB 或 BB,循環繼續。
若是 brother->left->color =RED,brother->right->color = BLACK,將其轉爲最後一種狀況一塊兒考慮。即將 brother->right 變紅。轉換步驟爲:將 brother->left->color = BLACK; brother->color = RED。這樣的話 brother 的左子樹多了一層黑,右旋 brother,恢復屬性。而後將 brother 指向如今的 parent 的右結點,那麼如今的 brother->right 就是紅色。轉爲最後一種狀況考慮。
若是 brother->right->color = RED。那麼就要將 brother->right 變黑,使得 brother 的黑色能夠上移而不破壞紅黑樹屬性,上移步驟是使 brother 變成 brother->parent 的顏色,brother->parent 變黑這樣一來,黑色就上移了。而後左旋 parent,這樣 successor 的額外黑就經過左旋加進來的黑色抵消了。可是 parent 的右子樹的黑高就 -1 了,而經過剛剛將 brother->right 變黑就彌補了右子樹減去的黑高。如今就不存在額外黑了,結束脩復,而後讓 successor 指向 root,判斷 root 是否爲紅色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
while node != root && node.color == BLACK) parent = node.parent if node = parent.left brother = parent.right if IS_RED(brother) brother.color = BLACK parent.color = RED LEFT_ROTATE(T, parent) brother = parent.right
if brother.left.color == BLACK and brother.right.color == BLACK brother.color = RED node = parent elseif brother.right.color = BLACK brother.left.color = BLACK brother.color = RED RIGHT_ROTATE(T, brother) brother = parent.right else brother.color = parent.color parent.color = BLACK brother.right.color = BLACK LEFT_ROTATE(T, parent) node = root else (same as then clause with 「right」 and 「left」 exchanged) node.color = BLACK |
刪除的操做主要有查找要刪除的結點,刪除以後的修復。
修復紅黑樹性質主要是旋轉和結點上移。對於查找來講,查找的算法複雜度是O(lgn),旋轉的複雜度是O(1),結點上移,走過的路徑最大值就是紅黑樹的高,所以上移結點的複雜度就是O(lgn)。
綜上所述,刪除算法的複雜度是 O(DELETE) = O(lgn) + O(1) + O(lgn) = O(lgn)
。
若是對部分步驟不理解,能夠到這個網站看看紅黑樹每一步操做的可視化過程:紅黑樹可視化網站。
本次代碼的實現請點擊:紅黑樹實現代碼
由於基礎知識比較薄弱,因此想補一下本身的基礎,無奈悟性較低,花了大半個月時間才把紅黑樹給理解和實現出來。中途跟朋友討論了不少次,所以有以上的這些總結。以前一直不敢去實現紅黑樹,由於以爲本身根本沒法理解和實現,心裏的恐懼一直壓抑着本身,但通過幾回掙扎以後,終於鼓起勇氣去研究一番,發現,只要用心去研究,就沒有解決不了的問題。糾結了好久要不要發這篇博文,這只是一篇知識筆記的記錄,並不敢說指導任何人,只想把本身在理解過程當中記錄下來的筆記分享出來,給有須要的人。但其實想一想,糾結個蛋,讓筆記做爲半成品躺在印象筆記裏沉睡,還不如花時間完善好發佈出來,而後有興趣的繼續探討一下。
若是真的要問我紅黑樹有什麼用?爲何要學它?我真的回答不上,可是我以爲,基礎的東西,多學一些也無妨。只有學了,有個思路在腦海裏,之後才能用得上,否則等真正要用纔來學的話,彷佛會浪費了不少學習成本。
http://blog.jobbole.com/103045/