STL源碼剖析RB-tree

一、紅黑樹概述

     紅黑樹和我們以前學過的AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。不過自從紅黑樹出來後,AVL樹就被放到了博物館裏,據說是紅黑樹有更好的效率,更高的統計性能。這一點在我們瞭解了紅黑樹的實現原理後,就會有更加深切的體會。
     紅黑樹和AVL樹的區別在於它使用顏色來標識結點的高度,它所追求的是局部平衡而不是AVL樹中的非常嚴格的平衡。學過數據結構的人應該都已經領教過AVL樹的複雜,但AVL樹的複雜比起紅黑樹來說簡直是小巫見大巫,紅黑樹纔是真正的變態級數據結構。
     由於STL中的關聯式容器默認的底層實現都是紅黑樹,因此紅黑樹對於後續學習STL源碼還是很重要的,有必要掌握紅黑樹的實現原理和源碼實現。
     紅黑樹是AVL樹的變種,紅黑樹通過一些着色法則確保沒有一條路徑會比其它路徑長出兩倍,因而達到接近平衡的目的。所謂紅黑樹,不僅是一個二叉搜索樹,而且必須滿足一下規則:
     1、每個節點不是紅色就是黑色。
     2、根節點爲黑色。
     3、如果節點爲紅色,其子節點必須爲黑色。
     4、任意一個節點到到NULL(樹尾端)的任何路徑,所含之黑色節點數必須相同。

上面的這些約束保證了這個樹大致上是平衡的,這也決定了紅黑樹的插入、刪除、查詢等操作是比較快速的。 根據規則4,新增節點必須爲紅色;根據規則3,新增節點之父節點必須爲黑色。當新增節點根據二叉搜索樹的規則到達其插入點時,卻未能符合上述條件時,就必須調整顏色並旋轉樹形,如下圖:

假設我們爲上圖分別插入節點3、8、35、75,根據二叉搜索樹的規則,插入這四個節點後,我們會發現它們都破壞了紅黑樹的規則,因此我們必須調整樹形,也就是旋轉樹形並改變節點的顏色。

二、紅黑樹上結點的插入

      在討論紅黑樹的插入操作之前必須要明白,任何一個即將插入的新結點的初始顏色都爲紅色。這一點很容易理解,因爲插入黑點會增加某條路徑上黑結點的數目,從而導致整棵樹黑高度的不平衡。但如果新結點的父結點爲紅色時(如下圖所示),將會違反紅黑樹的性質:一條路徑上不能出現相鄰的兩個紅色結點。這時就需要通過一系列操作來使紅黑樹保持平衡。

      爲了清楚地表示插入操作以下在結點中使用“新”字表示一個新插入的結點;使用“父”字表示新插入點的父結點;使用“叔”字表示“父”結點的兄弟結點;使用“祖”字表示“父”結點的父結點。插入操作分爲以下幾種情況:
1、黑父
     如下圖所示,如果新節點的父結點爲黑色結點,那麼插入一個紅點將不會影響紅黑樹的平衡,此時插入操作完成。紅黑樹比AVL樹優秀的地方之一在於黑父的情況比較常見,從而使紅黑樹需要旋轉的機率相對AVL樹來說會少一些。

2、紅父
     如果新節點的父結點爲紅色,這時就需要進行一系列操作以保證整棵樹紅黑性質。如下圖所示,由於父結點爲紅色,此時可以判定,祖父結點必定爲黑色。這時需要根據叔父結點的顏色來決定做什麼樣的操作。青色結點表示顏色未知。由於有可能需要根結點到新點的路徑上進行多次旋轉操作,而每次進行不平衡判斷的起始點(我們可將其視爲新點)都不一樣。所以我們在此使用一個藍色箭頭指向這個起始點,並稱之爲判定點。

2.1 紅叔
當叔父結點爲紅色時,如下圖所示,無需進行旋轉操作,只要將父和叔結點變爲黑色,將祖父結點變爲紅色即可。但由於祖父結點的父結點有可能爲紅色,從而違反紅黑樹性質。此時必須將祖父結點作爲新的判定點繼續向上(迭代)進行平衡操作。

需要注意的是,無論“父節點”在“叔節點”的左邊還是右邊,無論“新節點”是“父節點”的左孩子還是右孩子,它們的操作都是完全一樣的(其實這種情況包括4種,只需調整顏色,不需要旋轉樹形)。
2.2 黑叔
當叔父結點爲黑色時,需要進行旋轉,以下圖示了所有的旋轉可能:
Case 1:

Case 2:

Case 3:

Case 4:

      可以觀察到,當旋轉完成後,新的旋轉根全部爲黑色,此時不需要再向上回溯進行平衡操作,插入操作完成。需要注意,上面四張圖的“叔”、“1”、“2”、“3”結點有可能爲黑哨兵結點。

      其實紅黑樹的插入操作不是很難,甚至比AVL樹的插入操作還更簡單些。紅黑樹的插入操作源代碼如下:

三、元素的搜尋

對於一個二叉搜索樹而言,搜尋元素對於其而言可以稱之簡單,下面是尋找RB-tree中是否有鍵值爲k的節點:

// 尋找RBTree中是否存在鍵值爲k的節點
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>::find(const Key& k) {
  link_type y = header;        // Last node which is not less than k. 
  link_type x = root();        // Current node. 

  while (x != 0) 
    // key_compare是節點鍵值大小比較函數
    if (!key_compare(key(x), k)) 
      // 如果節點x的鍵值大於等於k,則繼續往左子樹查找  
      y = x, x = left(x);    // 
    else
      // 如果節點x的鍵值小於k,則繼續往右子樹查找
      x = right(x);
  iterator j = iterator(y); 
  // y的鍵值不小於k,返回的時候需要判斷與k是相等還是小於  
  return (j == end() || key_compare(k, key(j.node))) ? end() : j;
}

這裏原書中寫的是x大於k的時候向左,但是我理解的是大於等於向左走

即key_compare(key(x), k)表示key(x)小於k爲真。

這裏特別注意:只有當向左走的之前纔會保存當前節點到y中,就是爲了防止漏掉相等的情況。因爲這裏沒有添加相等的時候直接暫停

循環的語句。

(1)當find77的時候,k=77,while循環結束,y=77,x=0,

        return時,key_compare(k,key(j.node))爲false,所以返回j也就是y

(2)當find80的時候,x=80,y=80,x=75;

       75<80,x=77;

       77<80,x=0;while結束。

       return時,y=80.

因爲當x>=k的時候會向左走,避免漏掉等於的情況,所以只有向左走的時候記錄y。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章