紅黑樹(Red-black tree)

樹(tree)的基本知識

一.定義

是一種抽象數據類型,或是實作這種抽象數據類型的數據結構,用來模擬具有樹狀結構性質的數據集合

樹.png

二.特點

  • 每個節點有零個或多個子節點;
  • 沒有父節點的節點稱為根節點;
  • 每一個非根節點有且只有一個父節點;
  • 除了根節點外,每個子節點可以分為多個不相交的子樹

三.存儲結構

  1. 線性存儲
    /* 樹節點的定義 /
    #define MAX_TREE_SIZE 100
    typedef struct
    {
    TElemType data;
    int parent; /
    父節點位置域 /
    } PTNode;
    typedef struct
    {
    PTNode nodes[MAX_TREE_SIZE];
    int n; /
    節點數 */
    } PTree;
樹-線性存儲.jpg
  1. 鏈式存儲

     /*樹的孩子鏈表存儲表示*/
     typedef struct CTNode  // 孩子節點
     { 
         int child; 
         struct CTNode *next;
      } *ChildPtr;
      typedef struct
      {
         ElemType data; // 節點的數據元素 
         ChildPtr firstchild; // 孩子鏈表頭指針
       } CTBox;
       typedef struct 
       {
          CTBox nodes[MAX_TREE_SIZE]; 
          int n, r; // 節點數和根節點的位置
        } CTree;
    
樹-鏈式存儲.jpg

紅黑樹(Red-black tree)的基本知識

一.定義

紅黑樹是一種自平衡二叉查找樹,典型的用途是實現關聯數組,它是復雜的,但它的操作有著良好的最壞情況運行時間,并且在實踐中是高效的O(log n)時間內做查找,插入和刪除,這里的n是樹中元素的數目。

紅黑樹.png

NIL節點表示數據的結束,在wiki百科中其被當做葉子節點,畫圖時應該也體現出來。

二.特點

  1. 任意節點的左子樹不空,則左子樹上所有節點的值均小于它的根結點的值;
  2. 任意節點的右子樹不空,則右子樹上所有節點的值均大于它的根結點的值;
  3. 任意節點的左、右子樹也分別為二叉查找樹;
  4. 沒有鍵值相等的節點;
  5. 節點是紅色或黑色;
  6. 根是黑色, 所有葉子都是黑色;
  7. 每個紅色節點必須有兩個黑色的子節點;(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
  8. 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點;

通過節點顏色,限制了二叉樹的高度。
操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同于普通的二叉查找樹。

三.拋出問題

  • O(log n)的操作效率是如何得出的?為什么與高度成比例,而不是與節點數成比例?

一個由n個節點隨機構成的二叉查找樹的高度為(log n ).證明如下:

紅黑樹高度證明.jpg

而時間復雜度是以某個基礎數據操作的重復次數作為量度。紅黑樹的是二叉搜索樹,左子樹上所有節點的值均小于他的根節點的值,右子樹上所有節點均大于根節點的值,左右子節樹相對根節點按大小分布。如果把每次節點值的比較看成基礎數據操作,那么最差的查找情況是一直查找到高度最大的根節點,那么查找的時間復雜度即與高度成正比,可表示成O(log n)

如何生成紅黑樹

簡單了解了紅黑樹的字面定義,下面動手感受下紅黑樹的相關操作。當你插入或者刪除一個節點時,可能會破壞紅黑樹的性質,所以需要對樹節點進行重新著色或者旋轉,來保持紅黑樹的結構。首先看下二叉樹的旋轉。

一.旋轉

  • 左旋
樹左旋.jpg

假設pivot節點不為空,其右子樹不為空,那么左旋即是:使pivot的右孩子Y為子樹的根,pivot節點為子樹根節點的左孩子,pivot左孩子、Y節點的右孩子不改變,Y節點左孩子變為pivot節點右孩子。

  • 右旋
樹右旋.jpg

假設pivot節點不為空,其左子樹不為空,那么右旋:使pivot的左孩子Y為子樹的根,pivot節點為子樹根節點的右孩子,pivot的右孩子、Y節點的左孩子不變,Y節點的右孩子變為pivot節點的左孩子。

任意節點的左子樹不空,則左子樹上所有節點的值均小于它的根結點的值
任意節點的右子樹不空,則右子樹上所有節點的值均大于它的根結點的值

實戰演練之增加、刪除節點時,如何保證紅黑樹的性質不被破壞。

二.增加節點

往一個空的紅黑樹中,依次插入數據:12 1 9 2 0 11 7 19 4

  • 插入12
+12.jpg

節點為根節點,所以為黑色,兩個null節點為黑色節點。

  • 插入 1

+1.jpg

1小于12,所以是根節點的左孩子,如果為黑色,那么違反性質:從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。所以新增節點為紅色。

  • 插入 9
+9.jpg

按照二叉搜索樹的邏輯,9小于12、大于1,應該是1節點的右孩子。但,新增的兩個NIL節點已經使得12,1,9,NI這條路徑的黑色節點至少為兩個,而12,NIL這條路徑的黑色節點只有兩個。所以要對1節點進行左旋,9節點變為12節點的左孩子,發現問題還是存在。繼續,對12節點進行右旋,9節點為根節點,1、12分別為9節點的左右孩子。嘗試著色,9節點必須為黑色,而1,12節點可以為紅色,也可以為黑色。

  • 插入 2

+2.jpg

2大于1,直接作為1的右孩子即可。2的增加必然會增加兩個黑色NIL節點,所以每條路徑至少有三個黑色節點。則9,12,NIL這條路徑中,12節點應該變為黑色;9,1,NIL這條路徑中,1應該變為黑色。9,1,2,NIL這條路徑中,2為空色來使得黑色節點個數為三個。驗證一下,滿足從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。則顏色調整完畢。

  • 插入 0
+0.jpg

0節點直接作為1節點的左孩子,保持跟2節點相同的顏色即可。左右子樹依舊保持平衡。

  • 插入 11


    +11.jpg

    11節點作為12節點的左子樹,顏色跟同一層的0,2節點保持一致即可。

  • 插入 7
+7.jpg

從二叉查找樹的性質看,7節點作為2節點的右孩子即可。這時來分析著色問題,我們先看最短路徑的黑色分布,9,12,NIL這條路徑,有三個黑色節點,以此為參考,嘗試改變9節點左子樹的著色。目前最長的路徑是9,1,2,7,NIL這條路徑。保持三個黑色節點的話,9跟NIL已經為黑色節點,而紅色節點又不能挨著,所以只能是1為紅色節點,2為黑色節點,7為紅色節點。那么9,1,0,NIIL這條路徑,0就要為黑色節點。調整完畢。

  • 插入 19
+19.jpg

19節點作為12節點的右孩子,與左孩子保持一樣的紅色即可。

  • 插入 4
+4.jpg

4節點應該作為7節點的左子樹,無論著什么顏色,以1節點為根節點的子樹,都要破壞紅黑性質。所以應該進行旋轉。先以7為根節點進行一次右旋,再以2為根節點進行一次左旋。嘗試著色即可。

思維誤區:之前我把左右字數高度相差不能超過1加入到紅黑樹的性質中,這導致我的推理邏輯發生了錯誤。因為紅黑樹是用紅黑著色來保證高度,我直接用使用結果來推導紅黑著色,這樣會忽略紅黑樹本身的優點,而只關注了平衡二叉樹的優點。錯誤思維路線:我先優先保持二叉搜索樹的性質,然后通過旋轉使其保持平衡,然后再進行顏色調整。感覺只是在探討平衡二叉搜索樹的性質,紅黑性質被弱化了,僅僅是為了構造一個紅黑樹而調整著色。思維調整:先從二叉搜索樹的角度對節點進行插入,然后從著色的角度對樹進行旋轉。

插入節點的五種情況:
  1. 新節點N位于樹的根上,沒有父節點。
  2. 新節點的父節點P是黑色情形。
  3. 父節點P、叔叔節點U,都為紅色。
  4. 父節點P是紅色,叔叔節點U是黑色或NIL; 插入節點N是其父節點P的右孩子,而父節點P又是其父節點的左孩子。
  5. 父節點P是紅色,而叔父節點U 是黑色或NIL,要插入的節點N 是其父節點的左孩子,而父節點P又是其父G的左孩子。

這里,先不補充每種情況的操作方法,你是不是能自己動手寫下呢?歡迎留言~

三.刪除節點

類似插入節點的分析、總結,刪除節點也可以針對每種場景找到固定的著色方法,就像玩一個游戲,有自己的推理跟玩法。我先做個PPT,這塊稍后補充。

使用場景

所有的插入、刪除都是有限個情況,基于插入、刪除的情況分析,即可編寫算法生成紅黑樹,使其在固定的業務場景中發揮紅黑樹穩定操作效率的特色了。

  • 實現關聯數組
    關聯數組又稱映射Map)、字典Dictionary)是一個抽象的數據結構,它包含著類似于(鍵,值)的有序對。C++的STL中。map和set都是用紅黑樹實現的。
    底層采用紅黑樹保存 key-value對,Key值唯一。添加、取出都需要一些循環操作,但所有鍵值對都是按照key根據指定排序規則保持有序狀態。
  • 著名的linux進程調度Completely Fair Scheduler
    CFS用紅黑樹管理進程控制塊
    每個進程都包含運行時間,通過考慮優先級、系統負載等計算出的加權時間,作為紅黑樹的key。下一個執行的進程為最小運行時間的進程,對應紅黑樹的最左葉子。
  • epoll在內核中的實現,用紅黑樹管理事件塊
    linux內核的可擴展I/O事件通知機制,應用于高性能網絡程序場景,在內核cache中用紅黑樹儲存事件塊,保證較快的查詢速度。
  • nginx中,用紅黑樹進行超時管理
    Nginx為網頁服務器,在進行超時管理時,通過紅黑樹存儲超時時間對象,每次找到key最小的節點,然后進行判斷是否超時,超時就處理,直到取出的未超時的事件。
  • Java的TreeMap實現
    TreeMap是Java中key-value的集合,根據key值的自然順序進行排序,或者依據比較函數。

二叉搜索樹之間的對比

一.AVL樹

計算機科學中,AVL樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別為一,所以它也被稱為高度平衡樹。查找、插入和刪除在平均和最壞情況下都是O(log n)。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。
節點的平衡因子是它的左子樹的高度減去它的右子樹的高度(有時相反)。帶有平衡因子1、0或 -1的節點被認為是平衡的。帶有平衡因子 -2或2的節點被認為是不平衡的,并需要重新平衡這個樹。平衡因子可以直接存儲在每個節點中,或從可能存儲在節點中的子樹高度計算出來。

AVL旋轉.png

紅黑樹相對于AVL樹來說,犧牲了部分平衡性以換取插入/刪除操作時少量的旋轉操作,整體來說性能要優于AVL樹。

未完待續

不提問題的碼農不是好程序員。自己寫完了紅黑樹的簡單剖析,感覺還是只懂皮毛,沒有從觸碰到算法的核心內容。所以,不妨留幾個小問題,擔心自己腦子生銹或者沒事想玩手機的時候,再提筆研究下紅黑樹。

  • 舉例說明紅黑樹如何犧牲部分平衡性來保證盡可能少的旋轉操作,通過與AVL樹做比較。增加節點的時候,如果是在構建AVL樹,結果會怎么樣。

參考

教你初步了解紅黑樹
算法的時間復雜度和空間復雜度-總結
紅黑樹從頭至尾插入和刪除
AVL樹
紅黑樹C源碼實現與剖析

Echo
8 Nov,2016

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. 簡介 紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,是二叉查找樹的變種之一。它是在1972...
    謝樸歡閱讀 1,151評論 0 8
  • 樹的概述 樹是一種非常常用的數據結構,樹與前面介紹的線性表,棧,隊列等線性結構不同,樹是一種非線性結構 1.樹的定...
    Jack921閱讀 4,489評論 1 31
  • 1、紅黑樹介紹 紅黑樹又稱R-B Tree,全稱是Red-Black Tree,它是一種特殊的二叉查找樹,紅黑樹的...
    文哥的學習日記閱讀 9,932評論 1 35
  • 基于樹實現的數據結構,具有兩個核心特征: 邏輯結構:數據元素之間具有層次關系; 數據運算:操作方法具有Log級的平...
    yhthu閱讀 4,315評論 1 5
  • 這兩天,追完前段全民熱衷的電視劇《人民的名義》,55集,時間緊,任務重,很多地方都是跳過的。全劇追完,在兩個地方我...
    selinaselina閱讀 297評論 0 0