紅黑樹並沒有我們想象的那么難(上)


<紅黑樹並沒有我們想象的那么難> 上、下兩篇已經完成, 希望能幫助到大家.

紅黑樹並沒有想象的那么難, 初學者覺得晦澀難讀可能是因為情況太多. 紅黑樹的情況可以通過歸結, 通過合並來得到更少的情況, 如此可以加深對紅黑樹的理解. 網絡上的大部分紅黑樹的講解因為沒有「合並」. 紅黑樹的五個性質:

性質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. 所以有如下的印象圖:

rbtree0

插入節點的關鍵是:

  1. 插入新節點總是紅色節點
  2. 如果插入節點的父節點是黑色, 能維持性質
  3. 如果插入節點的父節點是紅色, 破壞了性質. 故插入算法就是通過重新着色或旋轉, 來維持性質

插入算法詳解如下, 走一遍紅黑樹維持其性質的過程:

第 0.0 種情況, N 為根節點, 直接 N->黑. over
第 0.1 種情況, N 的父節點為黑色, 這不違反紅黑樹的五種性質. over

第 1 種情況, N,P,U 都紅(G 肯定黑). 策略: G->紅, N,P->黑. 此時, G 紅, 如果 G 的父親也是紅, 性質又被破壞了, HACK: 可以將 GPUN 看成一個新的紅色 N 節點, 如此遞歸調整下去; 特俗的, 如果碰巧將根節點染成了紅色, 可以在算法的最后強制 root->黑.

rbtre01

第 2 種情況, P 為紅, N 為 P 右孩子, N 為紅, U 為黑或缺少. 策略: 旋轉變換, 從而進入下一種情況:

rbtree02

第 3 種情況, 可能由第二種變化而來, 但不是一定: P 為紅, N 為 P 左孩子, N 為紅. 策略: 旋轉, 交換 P,G 顏色, 調整后, 因為 P 為黑色, 所以不怕 P 的父節點是紅色的情況. over

rbtree03

紅黑樹的插入就為上面的三種情況. 你可以做鏡像變換從而得到其他的情況.

紅黑樹的刪除

假設 N 節點見上面普通二叉樹刪除中的定義, P 為 N 父節點, S 為 N 的兄弟節點, SL,SR 分別是 S 的左右子節點. 有如下印象圖:

rbtree04 N 沒有任何的孩子!

刪除節點的關鍵是:

  1. 如果刪除的是紅色節點, 不破壞性質
  2. 如果刪除的是黑色節點, 那么這個路徑上就會少一個黑色節點, 破壞了性質. 故刪除算法就是通過重新着色或旋轉, 來維持性質

刪除算法詳解如下, 走一遍紅黑樹維持其性質的過程:

第 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 節點! 這是遞歸原理.

rbtree05

第 2 情況, S 紅, 根據紅黑樹性質 P,SL,SR 一定黑. 策略: 旋轉, 交換 P,S 顏色. 處理后關注的范圍縮小, 下面的情況對應下面的框圖, 算法從框圖重新開始, 進入下一個情況:

rbtree06

第 2.1 情況, S,SL,SR 都黑. 策略: P->黑. S->紅, 因為通過 N 的路徑多了一個黑節點, 通過 S 的黑節點個數不變, 所以維持了性質 5. over. 將看到, sgi stl map 源代碼中將第 2.1 和第 1 情況合並成一種情況, 下節展開.

rbtree07

第 2.2.1 情況, S,SR 黑, SL 紅. 策略: 旋轉, 變換 SL,S 顏色. 從而又進入下一種情況:

rbtree08
第 2.2.2 情況, S 黑, SR 紅. 策略: 旋轉, 交換 S,P 顏色, SR->黑色, 重新獲得平衡.

rbtree09

上面情況標號 X.X.X 並不是說這些關系是嵌套的, 只是這樣展開容易理解. 此時, 解釋三個地方:

  1. 通過 N 的黑色節點數量多了一個
  2. 通過 SL 的黑色節點數量不變
  3. 通過 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

http://daoluan.net


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM