紅黑樹數據結構剖析

紅黑樹數據結構剖析 node

 

紅黑樹是計算機科學內比較經常使用的一種數據結構,它使得對數據的搜索,插入和刪除操做都能保持在O(lgn)的時間複雜度。然而,相比於通常的數據結構,紅黑樹的實現的難度有所增長。網絡上關於紅黑樹的實現資料汗牛充棟,可是乏於系統介紹紅黑樹實現的資料。本文經過一個本身實現的紅黑樹數據結構以及必要的搜索,插入和刪除操做算法,爲你們更系統地剖析紅黑樹數據結構的實現。git

對於大部分數據結構,通常都會使用抽象數據類型的方式實現,C++提供的模板機制能夠作到數據結構與具體數據類型無關,就像STL實現的那樣。不過本文並不是去實現STL中的紅黑樹,更重要的是透過紅黑樹的實現學習相關的算法和思想。固然,咱們仍是會借鑑STL中關於紅黑樹實現部分有價值內容。github

1、基本概念 算法

在具體實現紅黑樹以前,必須弄清它的基本含義。紅黑樹本質上是一顆二叉搜索樹,它知足二叉搜索樹的基本性質——即樹中的任何節點的值大於它的左子節點,且小於它的右子節點。windows

1 二叉搜索樹網絡

按照二叉搜索樹組織數據,使得對元素的查找很是快捷。好比圖1中的二叉搜索樹,若是查詢值爲48的節點,只須要遍歷4個節點便可完成。理論上,一顆平衡的二叉搜索樹的任意節點平均查找效率爲樹的高度h,即O(lgn)。可是若是二叉搜索樹的失去平衡(元素全在一側),搜索效率就退化爲O(n),所以二叉搜索樹的平衡是搜索效率的關鍵所在。爲了維護樹的平衡性,數據結構內出現了各類各樣的樹,好比AVL樹經過維持任何節點的左右子樹的高度差不大於1保持樹的平衡,而紅黑樹使用顏色的概念維持樹的平衡,使二叉搜索樹的左右子樹的高度差保持在固定的範圍。相比於其餘二叉搜索樹樹,紅黑樹對二叉搜索樹的平衡性維持有着自身的優點。數據結構

顧名思義,紅黑樹的節點是有顏色概念的,即非紅即黑。經過顏色的約束,紅黑樹維持着二叉搜索樹的平衡性。一顆紅黑樹必須知足如下幾點條件:函數

規則1、根節點必須是黑色。 學習

規則2、任意從根到葉子的路徑不包含連續的紅色節點。 測試

規則3、任意從根到葉子的路徑的黑色節點總數相同。

如圖2所示,爲一顆合法的紅黑樹,能夠發現紅黑樹在維持二叉搜索樹的基本性質的前提下,並知足了紅黑樹的顏色條件,總體上保持了二叉搜索樹的平衡性。(構造以下紅黑樹的數據序列爲:(503578275690454048),讀者能夠自行驗證。)

2 紅黑樹

2、數據結構設計

和通常的數據結構設計相似,咱們用抽象數據類型表示紅黑樹的節點,使用指針保存節點之間的相互關係。

做爲紅黑樹節點,其基本屬性有:節點的顏色、左子節點指針、右子節點指針、父節點指針、節點的值。

3 紅黑樹節點基本屬性

爲了方便紅黑樹關鍵算法的實現,還定義了一些簡單的操做(都是內聯函數)。

// 紅黑樹節點
template< class T>
class rb_tree_node
{
    typedef rb_tree_node_color node_color;
    typedef rb_tree_node<T> node_type;
public:
    node_color color; // 顏色
    node_type*parent; // 父節點
    node_type*left; // 左子節點
    node_type*right; // 右子節點
    T value; //
    rb_tree_node(T&v); // 構造函數
    inline node_type*brother(); // 獲取兄弟節點
    inline  bool on_left(); // 自身是左子節點
    inline  bool on_right(); // 自身是右子節點
    inline  void set_left(node_type*node); // 設置左子節點
    inline  void set_right(node_type*node); // 設置左子節點
};

爲了表示紅黑樹節點的顏色,咱們定義一個簡單的枚舉類型。

// 紅黑樹節點顏色
enum rb_tree_node_color
{
    red= false,
    black= true
};

有了節點,剩下的就是實現紅黑樹的構造、插入、搜索、刪除等關鍵算法了。

 

// 紅黑樹
template< class T>
class rb_tree
{
public:
    typedef rb_tree_node<T> node_type;
    rb_tree();
    ~rb_tree();
     void clear();
     void insert(T v); // 添加節點
     bool insert_unique(T v); // 添加惟一節點
    node_type* find(T v); // 查詢節點
     bool  remove(T v); // 刪除節點
    inline node_type* maximum(); // 最大值
    inline node_type* minimum(); // 最小值
    inline node_type* next(node_type*node); // 下一個節點
    inline node_type* prev(node_type*node); // 上一個節點
     void print(); // 輸出
     int height(); // 高度
    unsigned count(); // 節點數
     bool validate(); // 驗證
    unsigned get_rotate_times(); // 獲取旋轉次數
private:
    node_type*root; // 樹根
    unsigned rotate_times; // 旋轉的次數
    unsigned node_count; // 節點數
     void __clear(node_type*sub_root); // 清除函數
     void __insert(node_type*&sub_root,node_type*parent,node_type*node); // 內部節點插入函數
    node_type* __find(node_type*sub_root,T v); // 查詢
    inline node_type* __maximum(node_type*sub_root); // 最大值
    inline node_type* __minimum(node_type*sub_root); // 最小值
     void __rebalance(node_type*node); // 新插入節點調整平衡
     void __fix(node_type*node,node_type*parent, bool direct); // 刪除節點調整平衡
     void __rotate(node_type*node); // 自動判斷類型旋轉
     void __rotate_left(node_type*node); // 左旋轉    
     void __rotate_right(node_type*node); // 右旋轉
     void __print(node_type*sub_root); // 輸出
     int  __height(node_type*&sub_root); // 高度
     bool __validate(node_type*&sub_root, int& count); // 驗證紅黑樹的合法性
};

在紅黑樹類中,定義了樹根(root)和節點數(count),其中還記錄紅黑樹在插入刪除操做時執行的旋轉次數rotate_times。其中核心操做有插入操做(insert),搜索操做(find),刪除操做(remove),遞減操做(prev)——尋找比當前節點較小的節點,遞增操做(next)——尋找比當前節點較大的節點,最大值(maximum)和最小值(minimum)操做等。其中驗證操做(__ validate)經過遞歸操做紅黑樹,驗證紅黑樹的三個基本顏色約束,用於操縱紅黑樹後驗證紅黑樹是否保持平衡。

因爲插入和刪除操做是紅黑樹的關鍵所在,下邊重點介紹這兩個操做。其餘的操做通常經過對樹進行遞歸操做均可以輕鬆的完成,這裏再也不贅述。

3、紅黑樹的插入操做

紅黑樹的插入操做和查詢操做有些相似,它按照二分搜索的方式遞歸尋找插入點。不過這裏須要考慮邊界條件——當樹爲空時須要特殊處理(這裏未採用STL對樹根節點實現的特殊技巧)。若是插入第一個節點,咱們直接用樹根記錄這個節點,並設置爲黑色,不然做遞歸查找插入(__insert操做)。

默認插入的節點顏色都是紅色,由於插入黑色節點會破壞根路徑上的黑色節點總數,但即便如此,也會出現連續紅色節點的狀況。所以在通常的插入操做以後,出現紅黑樹約束條件不知足的狀況(稱爲失去平衡)時,就必需要根據當前的紅黑樹的狀況作相應的調整(__rebalance操做)。和AVL樹的平衡調整經過旋轉操做的實現相似,紅黑樹的調整操做通常都是經過旋轉結合節點的變色操做來完成的。

紅黑樹插入節點操做產生的不平衡來源於當前插入點和父節點的顏色衝突致使的(都是紅色,違反規則2)。

4 插入衝突

如圖4所示,因爲節點插入以前紅黑樹是平衡的,所以能夠判定祖父節點g必存在(規則1:根節點必須是黑色),且是黑色(規則2:不會有連續的紅色節點),而叔父節點u顏色不肯定,所以能夠把問題分爲兩大類:

1、叔父節點是黑色(如果空節點則默認爲黑色)

這種狀況下經過旋轉和變色操做可使紅黑樹恢復平衡。可是考慮當前節點n和父節點p的位置又分爲四種狀況:

Anp左子節點,pg的左子節點。

Bnp右子節點,pg的右子節點。

Cnp左子節點,pg的右子節點。

Dnp右子節點,pg的左子節點。

狀況AB統一稱爲外側插入,CD統一稱爲內側插入。之因此這樣分類是由於同類的插入方式的解決方式是對稱的,能夠經過鏡像的方法類似完成。

首先考慮狀況Anp左子節點,pg的左子節點。針對該狀況能夠經過一次右旋轉操做,並將p設爲黑色,g設爲紅色完成從新平衡。

5 左外側插入調整

右旋操做的步驟是:將p掛接在g節點原來的位置(若是g原是根節點,須要考慮邊界條件),將p的右子樹x掛到g的左子節點,再把g掛在p的右子節點上,完成右旋操做。這裏將最終旋轉結果的子樹的根節點做爲旋轉軸(p節點),也就是說旋轉軸在旋轉結束後稱爲新子樹的根節點!這裏須要強調一下和STL的旋轉操做的區別,STL的右旋操做的旋轉軸視爲旋轉以前的子樹根節點(g節點),不過這並不影響旋轉操做的效果。

類比之下,狀況B則須要使用左單旋操做來解決平衡問題,方法和狀況A相似。

6 右外側插入

接下來,考慮狀況Cnp左子節點,pg的右子節點。針對該狀況經過一次左旋,一次右旋操做(旋轉軸都是n,注意不是p),並將n設爲黑色,g設爲紅色完成從新平衡。

7 左內側插入

須要注意的是,因爲此時新插入的節點是n,它的左右子樹xy都是空節點,但即便如此,旋轉操做的結果須要將xy新的位置設置正確(若是不把pg的對應分支設置爲空節點的話,就會破壞樹的結構)。在以後的其餘操做中,待旋轉的節點n的左右子樹可能就不是空節點了。

類比之下,狀況D則須要使用一次右單旋,一次左單旋操做來解決平衡問題,方法和狀況C相似。

8 右內側插入

2、叔父節點是紅色

當叔父節點是紅色時,則不能直接經過上述方式處理了(把前邊的全部狀況的u節點看做紅色,會發現節點ug是紅色衝突的)。可是咱們能夠交換gpu節點的顏色完成當前衝突的解決。

9 叔父節點爲紅的插入

可是僅僅這樣作顏色交換是不夠的,由於祖父節點g的父節點(記做gp)若是也是紅色的話仍然會有衝突(ggp是連續的紅色,違反規則2)。爲了解決這樣的衝突,咱們須要從當前插入點n向根節點root回溯兩次。

第一次回溯時處理全部擁有兩個紅色節點的節點,並按照圖9中的方式交換父節點g與子節點pu的顏色,並暫時忽略gpp的顏色衝突。若是根節點的兩個子節點也是這種狀況,則在顏色交換完畢後從新將根節點設置爲黑色。

第二次回溯專門處理連續的紅色節點衝突。因爲通過第一遍的處理,在新插入點n的路徑上必定不存在同爲紅色的兄弟節點了。而仍出現gpp的紅色衝突時,gp的兄弟節點(gu)能夠判定爲黑色,這樣就回歸前邊討論的叔父節點爲黑色時的狀況處理。

10 消除連續紅色節點

因爲發生衝突的兩個紅色節點位置多是任意的,所以會出現上述的四種旋轉狀況。不過咱們把靠近葉子的紅色節點(g)看做新插入的節點,這樣面對AB狀況則把p的父節點gp做爲旋轉軸,旋轉後gp會是新子樹的根,而面對CD狀況時把p做爲旋轉軸便可,旋轉後p爲新子樹的根(所以能夠把四種旋轉方式封裝起來)。

在第二次回溯時,雖然每次遇到紅色衝突旋轉後都會提高ggp節點的位置(與根節點的距離減小),可是不管ggp誰是新子樹的根都不會影響新插入節點n到根節點root路徑的回溯,並且一旦新子樹的根到達根節點(parent指針爲空)就能夠中止回溯了。

經過以上的樹從新平衡策略能夠完美地解決紅黑樹插入節點的平衡問題。

4、紅黑樹的刪除操做

相比於插入操做,紅黑樹的刪除操做顯得更加複雜。不少資料都沒有將紅黑樹的刪除解釋清楚,清華的數據結構教材對紅黑樹刪除的描述也十分混亂,《STL源碼剖析》中侯sir對紅黑樹的刪除更是閉口不談。這裏參考了STL對紅黑樹刪除操做的實現方式,並作了適當的修改(紅黑樹使用哨兵節點表示空節點,而這裏使用空指針的方式,所以要杜絕空指針的引用問題)。

因爲紅黑樹就是二叉搜索樹,所以節點的刪除方式和二叉搜索樹相同。不過紅黑樹刪除操做的難點不在於節點的刪除,而在於刪除節點後的調整操做。所以紅黑樹的刪除操做分爲兩步,首先肯定被刪除節點的位置,而後調整紅黑樹的平衡性。

先考慮刪除節點的位置,若是待刪除節點擁有惟一子節點或沒有子節點,則將該節點刪除,並將其子節點(或空節點)代替自身的位置。若是待刪除節點有兩個子節點,則不能將該節點直接刪除。而是從其右子樹中選取最小值節點(或左子樹的最大值節點)做爲刪除節點(該節點必定沒有兩個子節點了,不然還能取更小的值)。固然在刪除被選取的節點以前,須要將被選取的節點的數據拷貝到本來須要刪除的節點中。選定刪除節點位置的狀況如圖11所示,這和二叉搜索樹的節點刪除徹底相同。

11 刪除點的選定

11中用紅色標記的節點表示被選定的真正刪除的節點(節點y)。其中綠色節點(yold)表示本來須要刪除的節點,而因爲它有兩個子節點,所以刪除y代替它,而且刪除y以前須要將y的值拷貝到yold,注意這裏若是是紅黑樹也不會改變yold的顏色!經過上述的方式,將全部的節點刪除問題簡化爲獨立後繼(或者無後繼)的節點刪除問題。而後再考慮刪除y後的紅黑樹平衡調整問題。因爲刪除y節點後,y的後繼節點n會做爲y的父節點p的孩子。所以在進行紅黑樹平衡調整時,np的子節點。

下邊考慮平衡性調整問題,首先考慮被刪除節點y的顏色。若是y爲紅色,刪除y後不會影響紅黑樹的平衡性,所以不須要作任何調整。若是y爲黑色,則y所在的路徑上的黑色節點總數減小1,紅黑樹失去平衡,須要調整。

y爲黑色時,再考慮節點n的顏色。若是n爲紅色,由於ny的惟一後繼,若是把n的顏色設置爲黑色,那麼就能恢復y以前所在路徑的黑色節點的總數,調整完成。若是n也是黑色,則須要按照如下四個步驟來考慮。

pn的父節點,wn節點的兄弟節點。假定np的左子節點,np的右子節點狀況能夠鏡像對稱考慮。

步驟1:若w爲紅色,則判定w的子節點(若是存在的話或者爲空節點)和節點p必是黑色(規則2)。此時將wp的顏色交換,並以w爲旋轉軸進行左旋轉操做,最後將w設定爲n的新兄弟節點(原來w的左子樹x)。

經過這樣的轉換,將本來紅色的w節點狀況轉換爲黑色w節點狀況。若w本來就是黑色(或者空節點),則直接進入步驟2

12 節點刪除狀況1

步驟2:不管步驟1是否獲得處理,步驟2處理的老是黑色的w節點,此時再考慮w的兩個子節點xy的顏色狀況。若是xy都是黑色節點(或者是空節點,若是父節點w爲空節點,認爲xy也都是空節點),此時將w的顏色設置爲紅色,並將n設定爲n的父節點p。此時,若是n爲紅色,則直接設定n爲黑色,調整結束。不然再次回到步驟1作類似的處理。注意節點n發生變化後須要從新設定節點wp

考慮因爲以前黑色節點刪除致使n的路徑上黑色節點數減1,所以能夠把節點n看做擁有雙重黑色的節點。經過此步驟將n節點上移,使得n與根節點距離減小,更極端的狀況是當n成爲根節點時,樹就能恢復平衡了(由於根節點不在意多一重黑色)。另外,在n的上移過程當中可能經過後續的轉換已經讓樹恢復平衡了。

13 節點刪除狀況2

步驟3:若是步驟2中的w的子節點不是全黑色,而是左紅(x紅)右黑(y黑)的話,將x設置爲黑色,w設置爲紅色,並以節點x爲旋轉軸右旋轉,最後將w設定爲n的新兄弟(原來的x節點)。

經過這樣的轉換,讓本來w子節點左紅右黑的狀況轉化爲左黑右紅的狀況。若w的右子節點本來就是紅色(左子節點顏色可黑可紅),則直接進入步驟4

14 節點刪除狀況3

步驟4:該步驟處理w右子節點y爲紅色的狀況,此時w的左子節點x可黑可紅。這時將w的右子節點y設置爲黑色,並交換w與父節點p的顏色(w原爲黑色,p顏色可黑可紅),再以w爲旋轉軸左旋轉,紅黑樹調整算法結束。

經過該步驟的轉換,能夠完全解決紅黑樹的平衡問題!該步驟的實質是利用左旋恢復節點n上的黑色節點總數,雖然pw雖然交換了顏色,但它們都是n的祖先,所以n路徑上的黑色節點數增長1。同時因爲左旋,使得y路徑上的黑色節點數減1,恰巧的是y的顏色爲紅,將y設置爲黑便能恢復y節點路徑上黑色節點的總數。

15 節點刪除狀況4

總結以上步驟,對紅黑樹節點刪除的平衡性調整概括爲以下流程。

16 節點刪除調整流程

經過上述的調整策略,能夠完美解決紅黑樹節點刪除時平衡性問題。

5、隨機測試

對數據結構準確性的測試主要考察如下操做:插入,刪除,查詢,遍歷和驗證。插入和刪除操做前邊作了充分的介紹,由insetremove實現,查詢操做在插入和刪除操做時會間接調用,由find實現,遍歷操做分爲正序(由minimumnext實現)和逆序遍歷(由maximimprev實現),驗證操做主要是驗證插入和刪除後紅黑樹的合法性(規則123),由validate實現。至於其餘和紅黑樹統計特性相關的操做,好比獲取樹高、節點數和累計的旋轉次數等能夠很容易實現。

咱們使用隨機數產生器隨機產生一批數據插入到紅黑樹內,而後再隨機產生一批數據做爲刪除操做的參數。其中每次插入和刪除時都會對樹的合法性進行驗證,而且在插入後刪除數據結束後以正序和逆序的方式輸出紅黑樹的節點以及其餘統計信息。測試代碼以下:

 

#include " rb_tree.h "
#include <time.h> 
#include <windows.h>

int main()
{
    srand((unsigned)GetCurrentTime());
     int times= 10,len= 30;
     while(times--)
    {
        rb_tree< int> tree;
         for( int i= 0;i<len;i++)
        {
             int num=rand()%len;
            tree.insert_unique(num);
             if(!tree.validate())cout<< " 插入時失去平衡 "<<endl;
        }
        cout<< " 正序: ";
         for(rb_tree< int>::node_type*node=tree.minimum();node;node=tree.next(node))
        {
            cout<<node->value<< "   ";
        }
        cout<< " \n旋轉次數-黑高-節點數: "<<tree.get_rotate_times()
            << "   "<<tree.height()<< "   "<<tree.count()<<endl;
        cout<< " 刪除: ";
         for( int i= 0;i<len;i++)
        {
             int num=rand()%len;
             if(tree.remove(num))cout<<num<< "   ";
             if(!tree.validate())cout<< " 刪除時失去平衡 "<<endl;
        }
        cout<<endl;
        cout<< " 逆序: ";
         for(rb_tree< int>::node_type*node=tree.maximum();node;node=tree.prev(node))
        {
            cout<<node->value<< "   ";
        }
        cout<< " \n旋轉次數-黑高-節點數: "<<tree.get_rotate_times()
            << "   "<<tree.height()<< "   "<<tree.count()<<endl;
        cout<< " ________________________________________________________________________________ "<<endl;
    }
     return  0;
}

通過大量的循環隨機測試,能夠驗證紅黑樹數據結構的穩定性以及平衡性調整算法的正確性,下邊是測試結果的部分截圖。

本文構造的紅黑樹數據結構源代碼下載地址爲:https://github.com/fanzhidongyzby/RBTree

讀者感興趣的話能夠下載驗證。

17 測試結果

綜上所述,咱們對紅黑樹數據結構有了更充分地瞭解,尤爲是複雜的紅黑樹的插入刪除平衡性調整算法,最後進行的測試驗證了紅黑樹的核心算法的正確性。經過對紅黑樹數據結構的詳盡剖析,相信你們對數據結構在計算機學科的重要性有了更充分地認識,但願本文對你有所幫助。

相關文章
相關標籤/搜索