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


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

SGI STL map 實現概述

根據上一節的紅黑樹分析, 結合 sgi stl map 的實現, 看看紅黑樹的源碼是如何實現的. 以下主要以代碼的注釋為主. sgi stl map 底層實現是 _Rb_tree類, 為了方便管理, _Rb_tree 內置了 _M_header, 用於記錄紅黑樹中的根節點, 最小節點和最大節點. 在插入刪除中都會對其進行維護. 找到一副美艷的圖片: rbtree_header

我只會展開插入和刪除的代碼. _Rb_tree 有 insert_unique() 和 insert_equal() 兩種, 前者不允許有重復值, 后者可以. insert_unique() 判斷是否有重復值的方法利用了二叉搜索樹的性質. 細節請參看下面的代碼.

為什么選擇紅黑樹作為底層實現

紅黑樹是一種類平衡樹, 但它不是高度的平衡樹, 但平衡的效果已經很好了. 補充說明另一種 AVL 樹, 我之前的博文: 《編程珠璣,字字珠璣》讀書筆記完結篇——AVL樹

用過 STL map 么, 你用過 linux 么(這個說大了), 他們都有紅黑樹的應用. 當你對搜索的效率要求較高,並且數據經常改動的情景,你可以用紅黑樹, 也就是 map.

至於, 為什么不用 AVL 樹作為底層實現, 那是因為 AVL 樹是高度平衡的樹, 而每一次對樹的修改, 都要 rebalance, 這里的開銷會比紅黑樹大. 紅黑樹插入只要兩次旋轉, 刪除至多三次旋轉. 但不可否認的是, AVL 樹搜索的效率是非常穩定的. 選取紅黑樹, 我認為是一種折中的方案.

紅黑樹源代碼剖析

// sgi stl _Rb_tree 插入算法 insert_equal() 實現.
// 策略概述: insert_equal() 在紅黑樹找到自己的位置,
// 然后交由 _M_insert() 來處理接下來的工作.
// _M_insert() 會將節點插入紅黑樹中, 接着調整紅黑樹,
// 維持性質.
template <class _Key, class _Value, class _KeyOfValue,
          class _Compare, class _Alloc>
typename _Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>::iterator
_Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>
  ::insert_equal(const _Value& __v)
{
  // 在紅黑樹中有頭結點和根節點的概念, 頭結點位於根節點之上,
  // 頭結點只為管理而存在, 根節點是真正存儲數據的地方. 頭結點和根節點互為父節點,
   // 是一種實現的技巧.
  _Link_type __y = _M_header; // 指向頭結點
  _Link_type __x = _M_root(); // _M_header->_M_parent, 即指向根節點

  // 尋找插入的位置
  while (__x != 0) {
    __y = __x;

    // 小於當前節點要走左邊, 大於等於當前節點走右邊
    __x = _M_key_compare(_KeyOfValue()(__v), _S_key(__x)) ?
            _S_left(__x) : _S_right(__x);
  }
  // __x 為需要插入的節點的位置, __y 為其父節點
  return _M_insert(__x, __y, __v);
}

// sgi stl _Rb_tree 插入算法 insert_unique() 實現.
// 策略概述: insert_unique() 同樣也在紅黑樹中找到自己的位置; 我們知道,
// 如果小於等於當前節點會往右走, 所以遇到一個相同鍵值的節點后, 會往右走一步,
// 接下來一直往左走, 所以下面的實現會對往左走的情況做特殊的處理.
template <class _Key, class _Value, class _KeyOfValue,
          class _Compare, class _Alloc>
pair<typename _Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>::iterator,
     bool>
_Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>
  ::insert_unique(const _Value& __v)
{
  _Link_type __y = _M_header; // 指向頭結點
  _Link_type __x = _M_root(); // 指向根節點, 可能為空
  bool __comp = true;

  // 尋找插入的位置
  while (__x != 0) {
    __y = __x;
    __comp = _M_key_compare(_KeyOfValue()(__v), _S_key(__x));

    // 小於當前節點要走左邊, 大於等於當前節點走右邊
    __x = __comp ? _S_left(__x) : _S_right(__x);
  }

  iterator __j = iterator(__y); // 在 __y 上建立迭代器

  // 我認為下面判斷樹中是否有存在鍵值的情況有點繞,
  // 它充分利用了二叉搜索樹的性質, 如此做很 hack, 但不易理解.
  // 要特別注意往左邊插入的情況.

  // HACKS:
  // 下面的 if 語句是比 __x 小走左邊的情況: 會發現, 如果插入一個已存在的鍵的話,
  // __y 最終會定位到已存在鍵的右子樹的最左子樹.
  // 譬如, 紅黑樹中已經存在一個鍵為 100 的節點, 其右孩子節點為 101,
  // 此時如果再插入鍵為 100 的節點, 因為 100<=100, 所以會往右走到達 101 節點,
  // 有 100<101, 繼而往左走, 會一直往左走.大家稍微畫一個例子就能理解.
  if (__comp)
    // 特殊情況, 如果 __j 指向了最左孩子, 那么肯定要插入新節點.
    if (__j == begin())
      return pair<iterator,bool>(_M_insert(__x, __y, __v), true);
    // 其他情況, 這個時候也是往左邊插入, 如果存在重復的鍵值,
    // 那么 --__j 能定位到此重復的鍵的節點.
    else
      --__j;

  // HACKS: 這里比較的是 __j 和 __v, 如果存在鍵值, 那么 __j == __v,
  // 會跳過 if 語句. 否則執行插入. 也就是說如果存在重復的鍵, 那么 __j
  // 的值肯定是等於 __v
  if (_M_key_compare(_S_key(__j._M_node), _KeyOfValue()(__v)))
    return pair<iterator,bool>(_M_insert(__x, __y, __v), true);

  // 此時 __y.value = __v, 不允許插入, 返回鍵值所在位置
  return pair<iterator,bool>(__j, false);
}

// _M_insert() 是真正執行插入的地方.
// 策略概述: 插入策略已經在上篇中詳述, 可以根據上篇文章的描述,
// 和下面代碼的注釋, 加深對紅黑樹插入算法里理解
template <class _Key, class _Value, class _KeyOfValue,
          class _Compare, class _Alloc>
typename _Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>::iterator
_Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>
  ::_M_insert(_Base_ptr __x_, _Base_ptr __y_, const _Value& __v)
{
  _Link_type __x = (_Link_type) __x_; // 新節點插入的位置.
  // 關於 __x 的疑問:
  // 1. 它被放到下面的, 第一個 if 語句中, 我覺得是沒有必要的,
  // 因為從調用 _M_insert() 的函數來看, __x 總是為空.
  // 2. 既然 __x 是新節點插入的位置, 那么為什么不直接在 __x 上創建節點,
  // 還要在下面通過比較來決定新節點是左孩子還是右孩子;
  // 不如直接用指針的指針或者指針的引用來完成, 省去了下面的判斷.

  _Link_type __y = (_Link_type) __y_; // 新節點的父節點
  _Link_type __z; // 新節點的位置

  if (__y == _M_header || __x != 0 ||
      _M_key_compare(_KeyOfValue()(__v), _S_key(__y))) {
  // 新節點應該為左孩子
    __z = _M_create_node(__v);
    _S_left(__y) = __z;               // also makes _M_leftmost() = __z
                                      //    when __y == _M_header
    if (__y == _M_header) {
      _M_root() = __z;
      _M_rightmost() = __z;
    }
    else if (__y == _M_leftmost())
      _M_leftmost() = __z;   // maintain _M_leftmost() pointing to min node
  }
  // 新節點應該為右孩子
  else {
    __z = _M_create_node(__v);
    _S_right(__y) = __z;
    if (__y == _M_rightmost())
      _M_rightmost() = __z;  // maintain _M_rightmost() pointing to max node
  }
  _S_parent(__z) = __y;
  _S_left(__z) = 0;
  _S_right(__z) = 0;

  // 重新調整
  _Rb_tree_rebalance(__z, _M_header->_M_parent);

  // 更新紅黑樹節點數
  ++_M_node_count;

  // 返回迭代器類型
  return iterator(__z);
}

// 插入新節點后, 可能會破壞紅黑樹性質, _Rb_tree_rebalance() 負責維持性質.
// 其中:
// __x 新插入的節點
// __root 根節點
// 策略概述: 紅黑樹插入重新調整的策略已經在上篇中講述,
// 可以結合上篇文章和這里的代碼注釋,
// 理解紅黑樹的插入算法.
inline void
_Rb_tree_rebalance(_Rb_tree_node_base* __x, _Rb_tree_node_base*& __root)
{
  // 將新插入的節點染成紅色
  __x->_M_color = _S_rb_tree_red;

  while (__x != __root && __x->_M_parent->_M_color == _S_rb_tree_red) {
    // __x 的父節點也是紅色的情況. 提示: 如果是黑色節點, 不會破壞紅黑樹性質.

    if (__x->_M_parent == __x->_M_parent->_M_parent->_M_left) {
      // 叔父節點
      _Rb_tree_node_base* __y = __x->_M_parent->_M_parent->_M_right;

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

        if (__x == __x->_M_parent->_M_right) {
        // 第 2 種情況, P 為紅, N 為 P 右孩子, U 為黑或缺少.
        // 策略: 旋轉變換, 從而進入下一種情況:
          __x = __x->_M_parent;
          _Rb_tree_rotate_left(__x, __root);
        }
        // 第 3 種情況, 可能由第二種變化而來, 但不是一定: P 為紅, N 為紅.
        // 策略: 旋轉, 交換 P,G 顏色, 調整后, 因為 P 為黑色, 所以不怕
        // P 的父節點是紅色的情況. over
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        _Rb_tree_rotate_right(__x->_M_parent->_M_parent, __root);
      }
    }
    else { // 下面的代碼是鏡像得出的, 腦補吧.
      _Rb_tree_node_base* __y = __x->_M_parent->_M_parent->_M_left;
      if (__y && __y->_M_color == _S_rb_tree_red) {
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __y->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        __x = __x->_M_parent->_M_parent;
      }
      else {
        if (__x == __x->_M_parent->_M_left) {
          __x = __x->_M_parent;
          _Rb_tree_rotate_right(__x, __root);
        }
        __x->_M_parent->_M_color = _S_rb_tree_black;
        __x->_M_parent->_M_parent->_M_color = _S_rb_tree_red;
        _Rb_tree_rotate_left(__x->_M_parent->_M_parent, __root);
      }
    }
  }
  __root->_M_color = _S_rb_tree_black;
}

// 刪除算法, 直接調用底層的刪除實現 _Rb_tree_rebalance_for_erase().
template <class _Key, class _Value, class _KeyOfValue,
          class _Compare, class _Alloc>
inline void _Rb_tree<_Key,_Value,_KeyOfValue,_Compare,_Alloc>
  ::erase(iterator __position)
{
  _Link_type __y =
    (_Link_type) _Rb_tree_rebalance_for_erase(__position._M_node,
                                              _M_header->_M_parent,
                                              _M_header->_M_left,
                                              _M_header->_M_right);
  destroy_node(__y);
  --_M_node_count;
}

// 刪除節點底層實現, 刪除可能會破壞紅黑樹性質,
// _Rb_tree_rebalance()
// 負責維持性質. 其中:
// __z 需要刪除的節點
// __root 根節點
// __leftmost 紅黑樹內部數據, 即最左子樹
// __rightmost 紅黑樹內部數據, 即最右子樹
// 策略概述: _Rb_tree_rebalance_for_erase() 會根據
// 刪除節點的位置在紅黑樹中找到頂替刪除節點的節點,
// 即無非是刪除節點左子樹的最大節點或右子樹中的最小節點,
// 此處用的是有一種策略. 接着, 會調整紅黑樹以維持性質.
// 調整的算法已經在上篇文章中詳述, 可以根據上篇文章的描述
// 和此篇的代碼注釋, 加深對紅黑樹刪除算法的理解.
inline _Rb_tree_node_base*
_Rb_tree_rebalance_for_erase(_Rb_tree_node_base* __z,
                             _Rb_tree_node_base*& __root,
                             _Rb_tree_node_base*& __leftmost,
                             _Rb_tree_node_base*& __rightmost)
{
  // __z 是要刪除的節點

  // __y 最終會指向要刪除的節點
  _Rb_tree_node_base* __y = __z;
  // N 節點
  _Rb_tree_node_base* __x = 0;
  // 記錄 N 節點的父節點
  _Rb_tree_node_base* __x_parent = 0;

  // 只有一個孩子或者沒有孩子的情況
  if (__y->_M_left == 0)     // __z has at most one non-null child. y == z.
    __x = __y->_M_right;     // __x might be null.
  else
    if (__y->_M_right == 0)  // __z has exactly one non-null child. y == z.
      __x = __y->_M_left;    // __x is not null.

    // 有兩個非空孩子
    else {                   // __z has two non-null children.  Set __y to
      __y = __y->_M_right;   //   __z's successor.  __x might be null.

      // __y 取右孩子中的最小節點, __x 記錄他的右孩子(可能存在右孩子)
      while (__y->_M_left != 0)
        __y = __y->_M_left;
      __x = __y->_M_right;
    }

  // __y != __z 說明有兩個非空孩子的情況,
  // 此時的刪除策略就和文中提到的普通二叉搜索樹刪除策略一樣:
  // __y 記錄了 __z 右子樹中最小的節點
  // __x 記錄了 __y 的右孩子
  // 用 __y 頂替 __z 的位置, __x 頂替 __y 的位置, 最后用 __y 指向 __z,
  // 從而 __y 指向了要刪除的節點
  if (__y != __z) {          // relink y in place of z.  y is z's successor

    // 將 __z 的記錄轉移至 __y 節點
    __z->_M_left->_M_parent = __y;
    __y->_M_left = __z->_M_left;

    // 如果 __y 不是 __z 的右孩子, __z->_M_right 有左孩子
    if (__y != __z->_M_right) {

      __x_parent = __y->_M_parent;

      // 如果 __y 有右孩子 __x, 必須有那個 __x 替換 __y 的位置
      if (__x)
        // 替換 __y 的位置
        __x->_M_parent = __y->_M_parent;

      __y->_M_parent->_M_left = __x;      // __y must be a child of _M_left
      __y->_M_right = __z->_M_right;
      __z->_M_right->_M_parent = __y;
    }
    // __y == __z->_M_right
    else
      __x_parent = __y;

    // 如果 __z 是根節點
    if (__root == __z)
      __root = __y;

    // __z 是左孩子
    else if (__z->_M_parent->_M_left == __z)
      __z->_M_parent->_M_left = __y;

    // __z 是右孩子
    else
      __z->_M_parent->_M_right = __y;

    __y->_M_parent = __z->_M_parent;
    // 交換需要刪除節點 __z 和 替換節點 __y 的顏色
    __STD::swap(__y->_M_color, __z->_M_color);
    __y = __z;
    // __y now points to node to be actually deleted
  }
  // __y != __z 說明至多一個孩子
  else {                        // __y == __z
    __x_parent = __y->_M_parent;
    if (__x) __x->_M_parent = __y->_M_parent;

    // 將 __z 的父親指向 __x
    if (__root == __z)
      __root = __x;
    else
      if (__z->_M_parent->_M_left == __z)
        __z->_M_parent->_M_left = __x;
      else
        __z->_M_parent->_M_right = __x;

    // __leftmost 和 __rightmost 是紅黑樹的內部數據, 因為 __z 可能是
    // __leftmost 或者 __rightmost, 因此需要更新.
    if (__leftmost == __z)
      if (__z->_M_right == 0)        // __z->_M_left must be null also
        // __z 左右孩子都為空, 沒有孩子
        __leftmost = __z->_M_parent;
    // makes __leftmost == _M_header if __z == __root
      else
        __leftmost = _Rb_tree_node_base::_S_minimum(__x);

    if (__rightmost == __z)
      if (__z->_M_left == 0)         // __z->_M_right must be null also
        __rightmost = __z->_M_parent;
    // makes __rightmost == _M_header if __z == __root
      else                      // __x == __z->_M_left
        __rightmost = _Rb_tree_node_base::_S_maximum(__x);

    // __y 同樣已經指向要刪除的節點
  }

  // __y 指向要刪除的節點
  // __x 即為 N 節點
  // __x_parent 指向 __x 的父親, 即 N 節點的父親
  if (__y->_M_color != _S_rb_tree_red) {
    // __y 的顏色為黑色的時候, 會破壞紅黑樹性質

    while (__x != __root && (__x == 0 || __x->_M_color == _S_rb_tree_black))
      // __x 不為紅色, 即為空或者為黑. 提示: 如果 __x 是紅色, 直接將 __x 替換成黑色

      if (__x == __x_parent->_M_left) { // 如果 __x 是左孩子

        _Rb_tree_node_base* __w = __x_parent->_M_right; // 兄弟節點

        if (__w->_M_color == _S_rb_tree_red) {
          //第 2 情況, S 紅, 根據紅黑樹性質P,SL,SR 一定黑.
          // 策略: 旋轉, 交換 P,S 顏色.

          __w->_M_color = _S_rb_tree_black;
          __x_parent->_M_color = _S_rb_tree_red; // 交換顏色
          _Rb_tree_rotate_left(__x_parent, __root); // 旋轉
          __w = __x_parent->_M_right; // 調整關系
        }

        if ((__w->_M_left == 0 ||
             __w->_M_left->_M_color == _S_rb_tree_black) &&
            (__w->_M_right == 0 ||
             __w->_M_right->_M_color == _S_rb_tree_black)) {
          // 提示: 這是 第 1 情況和第 2.1 情況的合並, 因為處理的過程是一樣的.
          // 但他們的情況還是要分門別類的. 已經在文章中詳細支出,
          // 似乎大多數的博文中沒有提到這一點.

          // 第 1 情況, N,P,S,SR,SL 都黑.
          // 策略: S->紅. 通過 PN,PS 的黑色節點數量相同了, 但會比其他路徑多一個,
          // 解決的方法是在 P 上從情況 0 開始繼續調整.
          // 為什么要這樣呢? HACKS: 因為既然 PN,PS
          // 路徑上的黑節點數量相同而且比其他路徑會少一個黑節點,
          // 那何不將其整體看成了一個 N 節點! 這是遞歸原理.

          // 第 2.1 情況, S,SL,SR 都黑.
          // 策略: P->黑. S->紅, 因為通過 N 的路徑多了一個黑節點,
          // 通過 S 的黑節點個數不變, 所以維持了性質 5. over

          // 可能大家會有疑問, 不對啊, 2.1 的情況,
          // 策略是交換父節點和兄弟節點的顏色, 此時怎么沒有對父節點的顏色賦值呢?
          // HACKS: 這就是合並情況的好處, 因為就算此時父節點是紅色,
          // 而且也將兄弟節點顏色改為紅色, 你也可以將 PS,PN 看成一個紅色的 N 節點,
          // 這樣在下一個循環當中, 這個 N 節點也會變成黑色. 因為此函數最后有一句話:
          // if (__x) __x->_M_color = _S_rb_tree_black;
          // 合並情況, 節省代碼量

          // 當然是可以分開寫的

          // 兄弟節點染成黑色
          __w->_M_color = _S_rb_tree_red;

          // 調整關系
          __x = __x_parent;
          __x_parent = __x_parent->_M_parent;
        } else {
          if (__w->_M_right == 0 ||
              __w->_M_right->_M_color == _S_rb_tree_black) {
            // 第 2.2.1 情況, S,SR 黑, SL 紅.
            // 策略: 旋轉, 變換 SL,S 顏色.

            if (__w->_M_left) __w->_M_left->_M_color = _S_rb_tree_black;
            __w->_M_color = _S_rb_tree_red;
            _Rb_tree_rotate_right(__w, __root);

            // 調整關系
            __w = __x_parent->_M_right;
          }

          // 第 2.2.2 情況, S 黑, SR 紅.
          // 策略: 旋轉, 交換 S,P 顏色, SR->黑色, 重新獲得平衡.
          __w->_M_color = __x_parent->_M_color;
          __x_parent->_M_color = _S_rb_tree_black;
          if (__w->_M_right) __w->_M_right->_M_color = _S_rb_tree_black;
          _Rb_tree_rotate_left(__x_parent, __root);
          break;
        }                        // 下面的代碼是鏡像得出的, 腦補吧.
      } else {                  // same as above, with _M_right <-> _M_left.
        _Rb_tree_node_base* __w = __x_parent->_M_left;
        if (__w->_M_color == _S_rb_tree_red) {
          __w->_M_color = _S_rb_tree_black;
          __x_parent->_M_color = _S_rb_tree_red;
          _Rb_tree_rotate_right(__x_parent, __root);
          __w = __x_parent->_M_left;
        }
        if ((__w->_M_right == 0 ||
             __w->_M_right->_M_color == _S_rb_tree_black) &&
            (__w->_M_left == 0 ||
             __w->_M_left->_M_color == _S_rb_tree_black)) {
          __w->_M_color = _S_rb_tree_red;
          __x = __x_parent;
          __x_parent = __x_parent->_M_parent;
        } else {
          if (__w->_M_left == 0 ||
              __w->_M_left->_M_color == _S_rb_tree_black) {
            if (__w->_M_right) __w->_M_right->_M_color = _S_rb_tree_black;
            __w->_M_color = _S_rb_tree_red;
            _Rb_tree_rotate_left(__w, __root);
            __w = __x_parent->_M_left;
          }
          __w->_M_color = __x_parent->_M_color;
          __x_parent->_M_color = _S_rb_tree_black;
          if (__w->_M_left) __w->_M_left->_M_color = _S_rb_tree_black;
          _Rb_tree_rotate_right(__x_parent, __root);
          break;
        }
      }
    if (__x) __x->_M_color = _S_rb_tree_black;
  }
  return __y;
}

搗亂 2013-9-29

http://daoluan.net


免責聲明!

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



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