<紅黑樹並沒有我們想象的那么難> 上、下兩篇已經完成, 希望能幫助到大家.
紅黑樹並沒有想象的那么難, 初學者覺得晦澀難讀可能是因為情況太多. 紅黑樹的情況可以通過歸結, 通過合並來得到更少的情況, 如此可以加深對紅黑樹的理解. 網絡上的大部分紅黑樹的講解因為沒有「合並」. 紅黑樹的五個性質:
性質1. 節點是紅色或黑色。
性質2. 根是黑色。
性質3. 所有葉子都是黑色(葉子是NIL節點)。
性質4. 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
性質5. 從任一節點到其每個葉子的所有簡單路徑 都包含相同數目的黑色節點。
紅黑樹的數據結構
摘自 sgi stl 紅黑樹數據結構定義:
typedef bool _Rb_tree_Color_type; const _Rb_tree_Color_type _S_rb_tree_red = false; const _Rb_tree_Color_type _S_rb_tree_black = true; struct _Rb_tree_node_base { typedef _Rb_tree_Color_type _Color_type; typedef _Rb_tree_node_base* _Base_ptr; _Color_type _M_color; _Base_ptr _M_parent; _Base_ptr _M_left; _Base_ptr _M_right; static _Base_ptr _S_minimum(_Base_ptr __x) { while (__x->_M_left != 0) __x = __x->_M_left; return __x; } static _Base_ptr _S_maximum(_Base_ptr __x) { while (__x->_M_right != 0) __x = __x->_M_right; return __x; } }; template <class _Value> struct _Rb_tree_node : public _Rb_tree_node_base { typedef _Rb_tree_node<_Value>* _Link_type; _Value _M_value_field; };
二叉搜索樹的插入刪除操作
在展開紅黑樹之前, 首先來看看普通二叉搜索樹的插入和刪除. 插入很容易理解, 比當前值大就往右走, 比當前值小就往左走. 詳細展開的是刪除操作.
二叉樹的刪除操作有一個技巧, 即在查找到需要刪除的節點 X; 接着我們找到要么在它的左子樹中的最大元素節點 M、要么在它的右子樹中的最小元素節點 M, 並交換(M,X). 此時, M 節點必然至多只有一個孩子; 最后一個步驟就是用 M 的子節點代替 M 節點就完成了. 所以, 所有的刪除操作最后都會歸結為刪除一個至多只有一個孩子的節點, 而我們刪除這個節點后, 用它的孩子替換就好了. 將會看到 sgi stl map 就是這樣的策略.
在紅黑樹刪除操作講解中, 我們假設代替 M 的節點是 N(下面的講述不再出現 M).
紅黑樹的插入
插入新節點總是紅色節點, 因為不會破壞性質 5, 盡可能維持所有性質.
假設, 新插入的節點為 N, N 節點的父節點為 P, P 的兄弟(N 的叔父)節點為 U, P 的父親(N 的爺爺)節點為 G. 所以有如下的印象圖:
插入節點的關鍵是:
- 插入新節點總是紅色節點
- 如果插入節點的父節點是黑色, 能維持性質
- 如果插入節點的父節點是紅色, 破壞了性質. 故插入算法就是通過重新着色或旋轉, 來維持性質
插入算法詳解如下, 走一遍紅黑樹維持其性質的過程:
第 0.0 種情況, N 為根節點, 直接 N->黑. over
第 0.1 種情況, N 的父節點為黑色, 這不違反紅黑樹的五種性質. over
第 1 種情況, N,P,U 都紅(G 肯定黑). 策略: G->紅, N,P->黑. 此時, G 紅, 如果 G 的父親也是紅, 性質又被破壞了, HACK: 可以將 GPUN 看成一個新的紅色 N 節點, 如此遞歸調整下去; 特俗的, 如果碰巧將根節點染成了紅色, 可以在算法的最后強制 root->黑.
第 2 種情況, P 為紅, N 為 P 右孩子, N 為紅, U 為黑或缺少. 策略: 旋轉變換, 從而進入下一種情況:
第 3 種情況, 可能由第二種變化而來, 但不是一定: P 為紅, N 為 P 左孩子, N 為紅. 策略: 旋轉, 交換 P,G 顏色, 調整后, 因為 P 為黑色, 所以不怕 P 的父節點是紅色的情況. over
紅黑樹的插入就為上面的三種情況. 你可以做鏡像變換從而得到其他的情況.
紅黑樹的刪除
假設 N 節點見上面普通二叉樹刪除中的定義, P 為 N 父節點, S 為 N 的兄弟節點, SL,SR 分別是 S 的左右子節點. 有如下印象圖:
刪除節點的關鍵是:
- 如果刪除的是紅色節點, 不破壞性質
- 如果刪除的是黑色節點, 那么這個路徑上就會少一個黑色節點, 破壞了性質. 故刪除算法就是通過重新着色或旋轉, 來維持性質
刪除算法詳解如下, 走一遍紅黑樹維持其性質的過程:
第 0.0 情況, N 為根節點. over
第 0.1 情況, 刪除的節點為紅. over
第 0.2 情況, 刪除節點為黑, N 為紅. 策略: N->黑, 重新平衡. over
第 1 情況, N,P,S,SR,SL 都黑. 策略: S->紅. 通過 PN,PS 的黑色節點數量相同了, 但會比其他路徑多一個, 解決的方法是在 P 上從情況 0 開始繼續調整. 為什么要這樣呢? HANKS: 因為既然 PN,PS 路徑上的黑節點數量相同而且比其他路徑會少一個黑節點, 那何不將其整體看成了一個 N 節點! 這是遞歸原理.
第 2 情況, S 紅, 根據紅黑樹性質 P,SL,SR 一定黑. 策略: 旋轉, 交換 P,S 顏色. 處理后關注的范圍縮小, 下面的情況對應下面的框圖, 算法從框圖重新開始, 進入下一個情況:
第 2.1 情況, S,SL,SR 都黑. 策略: P->黑. S->紅, 因為通過 N 的路徑多了一個黑節點, 通過 S 的黑節點個數不變, 所以維持了性質 5. over. 將看到, sgi stl map 源代碼中將第 2.1 和第 1 情況合並成一種情況, 下節展開.
第 2.2.1 情況, S,SR 黑, SL 紅. 策略: 旋轉, 變換 SL,S 顏色. 從而又進入下一種情況:
第 2.2.2 情況, S 黑, SR 紅. 策略: 旋轉, 交換 S,P 顏色, SR->黑色, 重新獲得平衡.
上面情況標號 X.X.X 並不是說這些關系是嵌套的, 只是這樣展開容易理解. 此時, 解釋三個地方:
- 通過 N 的黑色節點數量多了一個
- 通過 SL 的黑色節點數量不變
- 通過 SR 的黑色節點數量不變
紅黑樹刪除重新調整偽代碼如下:
// 第 0.0 情況, N 為根節點. over if N.parent == NULL: return; // 第 0.1 情況, 刪除的節點為紅. over if color == RED: return; // 第 0.2 情況, 刪除節點為黑, N 為紅, 簡單變換: N->黑, 重新平衡. over if color == BLACK && N.color == RED: N.color = BLACK; // 第 1 種情況, N,P,S,SR,SL 都黑. 策略: S->紅. 通過 N,S 的黑色節點數量相同了, 但會比其他路徑多一個, 解決的方法是在 P 上從情況 0 開始繼續調整. if N,P,S,SR,SL.color == BLACK: S.color = RED; // 調整節點關系 N = P N.parent = P.parent S = P.paernt.another_child SL = S.left_child SR = S.right_child continue; // 第 2 情況, S 紅, 根據紅黑樹性質 P,SR,SL 一定黑. 旋轉, 交換 P,S 顏色. 此時關注的范圍縮小, 下面的情況對應下面的框圖, 算法從框圖重新開始. if S.color == RED: rotate(P); swap(P.color,S.color); // 調整節點關系 S = P.another_child SL = S.left_child SR = S.right_child // 第 2.1 情況, S,SL,SR 都黑. 策略: P->黑. S->紅, 因為通過 N 的路徑多了一個黑節點, 通過 S 的黑節點個數不變, 所以維持了性質 5. over. 將看到, sgi stl map 源代碼中將第 2.1 和第 1 情況合並成一種情況, 下節展開. if S,SL,SR.color == BLACK: P.color = BLACK; S.color = RED; return // 第 2.2.1 情況, S,SR 黑, SL 紅. 策略: 旋轉, 變換 SL,S 顏色. 從而又進入下一種情況: if S,SR.color == BLACK && SL.color == RED: rotate(P); swap(S.color,SL.color); // 調整節點關系 S = SL SL = S.left_child SR = S.right_child // 第 2.2.2 情況, S 黑, SR 紅. 策略: 旋轉, 交換 S,P 顏色. if S.color == BLACK && SR.color == RED: rotate(P); swap(P.color,S.color); return;
總結
所以, 插入的情況只有三種, 刪除的情況只有兩種. 上面的分析, 做鏡像處理, 就能得到插入刪除的全部算法, 腦補吧. 從上面的分析來看, 紅黑樹具有以下特性: 插入刪除操作都是 0(lnN), 且最多旋轉三次.
下節中會重點展開 sgi stl map 的源代碼.
參考文檔: wikipedia
搗亂 2013-9-25