1. 程式人生 > >STL原始碼剖析 RB-tree

STL原始碼剖析 RB-tree

文章目錄

1. RB-tree概述

  • RB-tree,即紅黑樹,是一種平衡二叉搜尋樹,由二叉搜尋樹經過某種特定的操作使之能夠達到平衡

1.1 關於二叉搜尋樹

1.1.1 二叉搜尋樹簡介

二叉搜尋樹,顧名思義,是由一顆二叉樹組織而成,這樣的一顆樹可以由連結串列資料結構來表示,每個節點除了key以外,還有left、right及parent,分別指向左子節點、右子節點及父節點,如果對應的節點不存在的話則指向NIL節點

1.1.2 二叉搜尋樹的性質

二叉搜尋樹是一顆空樹或者具有以下性質的二叉樹:

  • 1.任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值
  • 2.任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值
  • 3.任意節點的左、右子樹也分別為二叉查詢樹
  • 4.沒有鍵值相等的節點

1.1.3 平衡二叉搜尋樹

我們知道,一顆由n個節點隨機構造的二叉搜尋樹的高度為logn,但也許因為輸入值不夠隨機,也許因為某些插入或刪除操作,二叉搜尋樹可能會失去平衡,造成搜尋效率低落的情況。從而,引出了平衡二叉搜尋樹的概念,這裡的平衡的意思是沒有任何一個節點深度過大,如AVL tree要求左右子樹高度相差最多1,AVL tree要求有點嚴苛,不過稍後所要解析的RB-tree並沒有如此的嚴苛,只是要求最長路徑不超過最短路徑的兩倍

1.2 RB-tree

經過剛剛的瞭解,我們知道RB-tree是一種平衡二叉搜尋樹,但RB-tree又不僅限於此,還必須滿足以下規則:

  • 1.每個節點不是紅色就是黑色
  • 2.根節點為黑色
  • 3.每個葉子節點(NIL)為黑色[注意:這裡所說的葉子節點是為空(NIL或NULL)的節點]
  • 4.父節點與子節點不能同時為紅色
  • 5.從一個節點到其子孫節點的路徑上所包含的黑節點數目相同

我們剛剛提到RB-tree要求其最長路徑不超過其最短路徑的兩倍,那麼通過上述的規則能否達到這個要求呢?通過每個節點的顏色限制是能夠達到這種要求的,例如,根據上述規則一顆RB-tree其最短路徑假設為n,即全為黑節點才能使得路徑最短,那麼根據規則5,我們就能得出滿足條件的最長路徑應該是黑紅相間的,且長度為2n或2n-1,這樣就滿足了條件

1.3 RB-tree平衡性修正

當我們在RB-tree中插入了一些資料時,就很容易導致RB-tree失衡,這個時候就需要某些特定操作來使得RB-tree重新恢復平衡,這些操作分為三種,分別為改變節點顏色、左旋和右旋,下面讓我們來逐一瞭解它們吧:

  1. 改變節點顏色
    首先,根據規則5,我們知道新增節點必須為紅色,而當此時的父節點恰好為紅色時,如果不進行更改,那麼顯然會違背規則4,這時就需要更改節點的顏色,如下圖所示
    在這裡插入圖片描述

  2. 將一個偏向右邊的紅色連結旋轉為左連結,如下圖在這裡插入圖片描述
    一個助於理解的動圖:
    在這裡插入圖片描述

  3. 將一個偏向左邊的紅色連結改為右連結,如下圖
    在這裡插入圖片描述
    一個有助於理解的動圖:
    在這裡插入圖片描述
    圖片參考自eson_15的部落格

現在我們認識到了RB-tree如何通過自我修正回到平衡,現在讓我們深入解析RB-tree在STL中是如何設計的吧!

2. 節點及迭代器設計

  • 首先,在解析slist提到過RB-tree採用與slist相同的設計方式,即雙層設計,那麼現在我們就來一探究竟RB-tree的雙層設計的內部結構

2.1 節點設計

//RB-tree特有的顏色定義
typedef bool __rb_tree_color_type;
const __rb_tree_color_type __rb_tree_red = false;  //紅色被定義為0
const __rb_tree_color_type __rb_tree_black = true;  //黑色被定義為1 
//RB-tree節點基本結構
struct __rb_tree_node_base {
	 typedef __rb_tree_color_type  color_type;
	 typedef __rb_tree_node_base* base_ptr;

	  color_type color;        // 節點顏色,非黑即紅
	  base_ptr parent;        // 指向父節點,由於RB-tree時常要上溯其父節點
 	  base_ptr left;         // 指向左子節點
 	  base_ptr right;        // 指向右子節點

	  // 一直往左走,就能找到紅黑樹的最小值節點
  	  // 二叉搜尋樹的性質
	  static base_ptr minimum(base_ptr x)
	  {
  		  while (x->left != 0) x = x->left;
   		  return x;
	  }
	  // 一直往右走,就能找到紅黑樹的最大值節點
 	  // 二叉搜尋樹的性質
	  static base_ptr maximum(base_ptr x)
	  {
   		 	while (x->right != 0) x = x->right;
    		return x;
 	 }
};

// 真正的節點定義,採用雙層節點結構
// 基類中不包含模板引數
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base
{
 	   typedef __rb_tree_node<Value>* link_type;
	    Value value_field;    // 即節點值
};

2.2 迭代器設計

2.2.1 header設計

  • 在瞭解RB-tree迭代器設計之前,首先了解header這一特殊設計:
  1. 樹狀結構的各種操作,最需注意的就是邊界情況的發生,也就是走到根節點時要有特殊的處理,為了簡化這種處理,SGI STL為根節點再設計了一個父節點,名為header
    在這裡插入圖片描述
  2. 當插入一個節點時,不但要按照RB-tree的規則來調整,並且維護header的正確性,使其父節點指向根節點,左子節點指向最小節點,右子節點指向最大節點

2.2.2 設計迭代器

struct __rb_tree_base_iterator
{
  	 typedef __rb_tree_node_base::base_ptr base_ptr;
 	 typedef bidirectional_iterator_tag iterator_category;
 	 typedef ptrdiff_t difference_type;

 	 base_ptr node;    // 用來連線紅黑樹的節點

 	 // 尋找該節點的後繼節點上
  	 void increment()
     {
         if (node->right != 0) { // 如果存在右子節點
         	 node = node->right;       // 直接跳到右子節點上
     	 	 while (node->left != 0) // 然後一直往左子樹走,直到左子樹為空
       		 node = node->left;
         }
  		 else {                    // 沒有右子節點
  	   	  	base_ptr y = node->parent;    // 找出父節點
     	    while (node == y->right) {    // 如果該節點一直為它的父節點的右子節點
       		  	 node = y;                       // 就一直往上找,直到不為右子節點為止
        		 y = y->parent;
         	}
       	    if (node->right != y)      // 若此時該節點不為它的父節點的右子節點
              	 node = y;                // 此時的父節點即為要找的後繼節點
                                 // 否則此時的node即為要找的後繼節點,此為特殊情況,如下
                                 // 我們要尋找根節點的下一個節點,而根節點沒有右子節點
                                 // 此種情況需要配合rbtree的header節點的特殊設計,後面會講到
      	 }                        
 	 }

  	// 尋找該節點你的前置節點
	  void decrement()
  	  {
 		   if (node->color == __rb_tree_red && // 如果此節點是紅節點
       			 node->parent->parent == node)       // 且父節點的父節點等於自己
     			 node = node->right;                               // 則其右子節點即為其前置節點
    // 以上情況發生在node為header時,即node為end()時
    // 注意:header的右子節點為mostright,指向整棵樹的max節點,後面會有解釋
   		   else if (node->left != 0) {                 // 如果存在左子節點
   	 			  base_ptr y = node->left;            // 跳到左子節點上
      			  while (y->right != 0)               // 然後一直往右找,知道右子樹為空
                        y = y->right;           
                   node = y;                          // 則找到前置節點
  		   }
    	   else {                                   // 如果該節點不存在左子節點
     			 base_ptr y = node->parent;         // 跳到它的父節點上
     			 while (node == y->left) {          // 如果它等於它的父子節點的左子節點
        				node = y;                   // 則一直往上查
      				    y = y->parent;                                  
     	  		 }                               // 直到它不為父節點的左子節點未知
      	  		 node = y;                       // 此時他的父節點即為要找的前驅節點
   	   	   }
  	   }
};

template <class Value, class Ref, class Ptr>
struct __rb_tree_iterator : public __rb_tree_base_iterator
{
	 //...型別宣告
     // 迭代器的建構函式
 	 __rb_tree_iterator() {}
 	 __rb_tree_iterator(link_type x) { node = x; }
 	 __rb_tree_iterator(const iterator& it) { node = it.node; }
  	// 提領和成員訪問函式,過載了*和->操作符
     reference operator*() const { return link_type(node)->value_field; }
 	 pointer operator->() const { return &(operator*()); }
	  // 前置++和後置++
	  self& operator++() { increment(); return *this; }
	  self operator++(int) {
  	  	self tmp = *this;
  	 	increment();        // 直接呼叫increment函式
   		return tmp;
 	  }
 	 // 前置--和後置--
	  self& operator--() { decrement(); return *this; }
 	  self operator--(int) {
   		  self tmp = *this;
  		  decrement();        // 直接呼叫decrement函式
  		  return tmp;
 	 }
};
  • 以下是對於這兩個函式的個人理解(忽略圖畫的不好orz):
    在這裡插入圖片描述

3. RB-tree資料結構

  • RB-tree定義如下,我們可以觀察到一些型別的定義,用來維護RB-tree的三筆資料(其中包含一個仿函式,用來比較節點之間的大小),以及一些member function的定義或宣告:
template <class Key, class Value, class KeyOfValue, class Compare,
          class Alloc = alloc>
class rb_tree {
protected:
  typedef void* void_pointer;
  typedef __rb_tree_node_base* base_ptr;
  typedef __rb_tree_node<Value> rb_tree_node;       
  typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator; // 專屬配置器
  typedef __rb_tree_color_type color_type;
public:
    // 一些型別宣告
  typedef Key key_type;
  typedef Value value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef rb_tree_node* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
protected:
  // RB-tree的資料結構
  size_type node_count; // 記錄樹的節點個數
  link_type header;         // header節點設計
  Compare key_compare;  // 節點間的鍵值大小比較準則

  // 以下三個函式用來取得header的成員
  link_type& root() const { return (link_type&) header->parent; }
  link_type& leftmost() const { return (link_type&) header->left; }
  link_type& rightmost() const { return (link_type&) header->right; }

  // 以下六個函式用來取得節點的成員
  static link_type& left(link_type x) { return (link_type&)(x->left); }
  static link_type& right(link_type x) { return (link_type&)(x->right); }
  static link_type& parent(link_type x) { return (link_type&)(x->parent); }
  static reference value(link_type x) { return x->value_field; }
  static const Key& key(link_type x) { return KeyOfValue()(value(x)); }
  static color_type& color(link_type x) { return (color_type&)(x->color); }

  // 以下六個函式用來取得節點的成員,由於雙層設計,導致這裡需要兩個定義
  static link_type& left(base_ptr x) { return (link_type&)(x->left); }
  static link_type& right(base_ptr x) { return (link_type&)(x->right); }
  static link_type& parent(base_ptr x) { return (link_type&)(x->parent); }
  static reference value(base_ptr x) { return ((link_type)x)->value_field; }
  static const Key& key(base_ptr x) { return KeyOfValue()(value(link_type(x)));} 
  static color_type& color(base_ptr x) { return (color_type&)(link_type(x)->color); }

  // 求取極大值和極小值,這裡直接呼叫節點結構的函式極可
  static link_type minimum(link_type x) { 
    return (link_type)  __rb_tree_node_base::minimum(x);
  }
  static link_type maximum(link_type x) {
    return (link_type) __rb_tree_node_base::maximum(x);
  }

public:
    // RBTree的迭代器定義
  typedef __rb_tree_iterator<value_type, reference, pointer> iterator;
  typedef __rb_tree_iterator<value_type, const_reference, const_pointer> 
          const_iterator;
private:
	//...
	void init() {   //構造一個空tree
		header = get_node();   //產生一個節點空間,令header指向它
		color(header) = __rb_tree_red;  //令header為紅色,用來將
		                                //root與header區分開
		root() = 0;          
		leftmost() = header;       //header的左子節點為自己
		rightmost() = header;      //header的右子節點為自己
	}
public:
  Compare key_comp() const { return key_compare; }  // 由於紅黑樹自帶排序功能,所以必須傳入一個比較器函式
  iterator begin() { return leftmost(); }        // RBTree的起始節點為左邊最小值節點
  const_iterator begin() const { return leftmost(); }
  iterator end() { return header; }                         // RBTree的終止節點為右邊最大值節點
  const_iterator end() const { return header; }
  bool empty() const { return node_count == 0; }    // 判斷紅黑樹是否為空    
  size_type size() const { return node_count; }     // 獲取紅黑樹的節點個數
  size_type max_size() const { return size_type(-1); }  // 獲取紅黑樹的最大節點個數,
                                                  // 沒有容量的概念,故為sizetype最大值
};

我們可以觀察到,相較於其他的資料結構,其實RB-tree並無多少複雜,而對於header我們在上面已經解析了,稍後仍然會提及,所以這裡還是沒有什麼難點的,接下來讓我們來了解一些關於RB-tree的構造

4. RB-tree構造與記憶體管理

以下是RB-tree關於構造的一些例項:

  • 1.在上述RB-tree資料結構定義中我們看到了RB-tree定義了一個專屬的空間配置器rb_tree_node_allocator為其配置節點
  • 2.在上述資料結構中我們未列上去的關於節點的函式get_node(),put_node(),create_node(),clone_node,destroy_node()
  • 3.RB-tree構造方式:以現有的RB-tree複製一個新的RB-tree;產生一顆空空的樹
//例:產生一顆空空如也的樹
rb_tree<int, int, identity<int>, less<int> > itree;  //定義了節點的鍵值、實值及大小比較準則
//呼叫預設建構函式
rb_tree(const Compare& comp = Compare())
	: node_count(0), key_compare(comp)  {  init();  }
//呼叫init初始化
void init() {   //構造一個空tree
		header = get_node();   //產生一個節點空間,令header指向它
		color(header) = __rb_tree_red;  //令header為紅色,用來將
		                                //root與header區分開
		root() = 0;          
		leftmost() = header;       //header的左子節點為自己
		rightmost() = header;      //header的右子節點為自己
}
  • 4.關於header設計,主要是簡化到邊界特殊情況的處理,在上述已經詳細解析了,這裡就不再重提了

5. 元素操作

5.1 元素插入

在前面我們已經知道了RB-tree對於平衡性修正有三種方式,分別是改變節點顏色、左旋和右旋,我們也知道在插入新元素時會導致RB-tree失去平衡,那麼我們應該怎樣合適的應用這三種方式來使RB-tree恢復平衡呢?現在就讓我們來一起解析吧!

  1. 先來了解一下RB-tree提供的兩種插入操作:insert_unique()insert_equal()
    insert_unique():表示被插入的鍵值在整棵樹中必須獨一無二
    insert_equal():表示被插入節點的鍵值在整棵樹中可以重複
  • insert_unique():
// 此插入函式不允許重複
// 返回的是一個pair,第一個元素為紅黑樹的迭代器,指向新增節點
// 第二個元素表示插入操作是否成功的
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)  
{  
    rb_tree_node* y = header;    // 根節點root的父節點  
    rb_tree_node* x = root();    // 從根節點開始  
    bool comp = true;  
    while(x != 0)  
    {  
        y = x;  
        comp = key_compare(KeyOfValue()(v) , key(x));    // v鍵值小於目前節點之鍵值?  
        x = comp ? left(x) : right(x);   // 遇“大”則往左,遇“小於或等於”則往右  
    }  
    // 離開while迴圈之後,y所指即插入點之父節點(此時的它必為葉節點)  
    iterator j = iterator(y);     // 令迭代器j指向插入點之父節點y  
    if(comp)     // 如果離開while迴圈時comp為真(表示遇“大”,將插入於左側)  
    {  
        if(j == begin())    // 如果插入點之父節點為最左節點  
            return pair<iterator , bool>(_insert(x , y , z) , true);// 呼叫_insert函式
        else     // 否則(插入點之父節點不為最左節點)  
            --j;   // 調整j,回頭準備測試  
    }  
    if(key_compare(key(j.node) , KeyOfValue()(v) ))  
        // 新鍵值不與既有節點之鍵值重複,於是以下執行安插操作  
        return pair<iterator , bool>(_insert(x , y , z) , true);  
    // 以上,x為新值插入點,y為插入點之父節點,v為新值  

    // 進行至此,表示新值一定與樹中鍵值重複,那麼就不應該插入新值  
    return pair<iterator , bool>(j , false);  
} 
  • insert_equal():
//插入新值:節點鍵值允許重複
//返回值是一個RB-tree迭代器,指向新增節點
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_equal(const Value &v)  
{
	link_type y = header;
	link_type x = root();   //從根節點開始
	while (x != 0) {             //從根節點開始,往下尋找合適的插入點
		y  = x;
		x = key_compare(KeyOfValue()(v), key(x))