簡單聊聊紅黑樹(Red Black Tree)

â红ä¸é»âçå¾çæç´¢ç»æ

 

前言html

衆所周知,紅黑樹是很是經典,也很很是重要的數據結構,自從1972年被髮明以來,由於其穩定高效的特性,40多年的時間裏,紅黑樹一直應用在許多系統組件和基礎類庫中,默默無聞的爲咱們提供服務,身邊有不少同窗常常問紅黑樹是怎麼實現的,因此在這裏想寫一篇文章簡單和你們聊聊下紅黑樹java

 

小編看過不少講紅黑樹的文章,都不是很容易懂,主要也是由於完整的紅黑樹很複雜,想經過一篇文章來講清楚實在很難,因此在這篇文章中我想盡可能用通俗口語化的語言,再結合 Robert Sedgewick 在《算法》中的改進的版本(2-3樹版本,容易理解也方便實現),能夠保證讓你們對紅黑樹的原理有大概的理解面試

 

其實對於大部分同窗來講,大概瞭解紅黑樹的工做原理就基本夠用了,由於一般不會有面試官讓你去手寫紅黑樹,你也幾乎不須要去本身實現一個紅黑樹,看完這裏,若是感受意猶未盡,還有興趣的同窗能夠去看看《算法導論》的紅黑樹實現,那是完整的4階B樹(2-3-4樹)版本的實現算法

 

關於紅黑樹的主題,咱們的文章有如下的靈魂三問:swift

  • 爲何會有紅黑樹?數組

  • 紅黑樹的應用場景和定義?數據結構

  • 紅黑樹的高效和穩定是怎麼實現?app

 

爲何會有紅黑樹工具

要了解紅黑樹,先它的前輩:二叉樹,平衡二叉樹(咱們的讀者應該都具有這些前置知識,因此咱們只作大概的講解)性能

前置知識:

二叉樹:傳統的數組和鏈表等線性結構表效率低下,線性表在處理大規模數據的時間複雜度都是線性級別 O(n),因此這種低效的數據結構,幾乎不可能用來處理千萬級別或者以上的數據量,因而基於二分思想的二叉樹就誕生了,在最好狀況下,二叉樹查找的時間複雜度能夠達到恐怖的對數級別 O(logN),什麼概念呢?就是在十億級別的數據量裏面,二叉樹只須要15~30次的訪問就能夠找到目標,固然咱們的前提是最好狀況,那麼最壞狀況呢?能夠參考下圖

 

二叉樹的最好/最壞狀況:

 

上圖能夠看到,二叉樹的性能的好壞,依賴數據的插入順序,最壞狀況下二叉樹會退化爲鏈表,全部操做的時間複雜度回到的線性級別 O(n),那麼怎麼解決這個問題呢?

 

想要讓樹的查找效率最大化,那麼就要保持樹的平衡,因此平衡二叉樹出現了,平衡二叉樹的思想是在操做的時候對樹進行平衡調整,來防止二叉樹退化爲鏈表,從而保證二叉樹的最優查找性能,完美的平衡二叉樹對高度的定義是相差不會大於1,這就至關於每次都插入/刪除操做,都會對樹進行平衡操做,這是代價很是高的操做,你能夠理解爲,相似數組爲了保證有序性,數組中間插入數據,全部元素都要向後移動的代價,雖然名字叫 平衡二叉樹,其實它的性能很是不平衡,由於它是最大化 插入/刪除 操做的時間來換取 查找 操做的時間最小化

 

看到這裏,就有好奇的同窗問,那麼有沒有既能夠保證樹的完美平衡,又能夠保證全部操做性能的數據結構呢?能夠很負責任的告訴你,有的,就是紅黑樹,咱們先看看紅黑樹能爲咱們帶來什麼?

  • 紅黑樹能夠保證 全部操做時間複雜度都是對數級別 O(logN)

  • 和二叉樹不一樣,不管插入順序如何,紅黑樹都是接近完美平衡的

  • 無數實驗的應用證實,紅黑樹的操做成本比二叉樹下降40%左右

 

常見樹形結構的操做複雜度對比,能夠看到紅黑樹是最均衡的:

âäºåæ  çº¢é»æ  æ¶é´å¤æ度âçå¾çæç´¢ç»æ

 

紅黑樹的應用場景和定義

 

簡單羅列下咱們經常使用的哪些工具是經過紅黑樹實現的

  • Java 的 HashMap (8 之後)的鏈表樹化是經過 紅黑樹實現

  • Java 的 TreeMap 是經過紅黑樹實現

  • Nginx 用紅黑樹管理 timer 等

  • Linux 進程調度用紅黑樹管理進程控制塊

  • 等等……

     

紅黑樹的定義,標準的紅黑樹示意圖:

 

紅黑樹自己是二叉樹,其背後的思想是使用二叉樹的結構再加載額外的顏色信息,來表示2-3樹,因此紅黑樹是包含了二叉樹的高效查找和2-3樹的高效插入平衡優勢的算法

 

在咱們討論的版本中對紅黑樹的定義以下:

  • 紅連接必須爲左連接

  • 不能出現兩條相連的紅連接

  • 該樹是完美黑色平衡的

 

只看這些定義你可能會以爲描述很是的學院派,很差理解,咱們先看看標準的紅黑樹,後面再用畫圖的方式來逐漸講解

 

紅黑樹插入維護規則的核心代碼

    private Node put(Node h, Key key, Value val) {
        // 二分插入
        if(h == null) return new Node(key, val, RED, 1);
        int cmp = key.compareTo(h.key);
        if(cmp < 0) h.left = put(h.left, key, val);
        else if(cmp > 0) h.right = put(h.right, key, val);
        else h.val = val;
 
        // 修復 右傾鏈接
        if(isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);         // 違反規則 不容許出現右紅鏈接
        if(isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);     // 違反規則 不容許出現連續的左紅鏈接
        if(isRed(h.left) && isRed(h.right)) flipColors(h);              // 當左右子節點爲紅色, 則變色
        h.size = size(h.left) + size(h.right) + 1;
        return h;
    }

 

 

紅黑樹的高效和穩定是怎麼實現?

 

在插入數據的過程當中紅黑樹會出現不少違反上面定義的狀況,若是出現違反紅黑樹定義的狀況,那麼就依靠紅黑樹的三個核心操做來保證樹的平衡,這三個操做也對應了紅黑樹定義的三條規則,分別以下:

  • 左旋轉(當出現右紅子節點時,進行左旋轉)

  • 右旋轉(當出現兩條相連的左子紅連接時,進行右旋轉)

  • 變色(當左右節點都是紅連接時,進行變色)

 

左旋轉

將紅色的右節點,調整到樹的左邊,假如我要在樹的底部插入元素S,可是元素被分配到的元素E的右邊,具體以下:

左旋轉是針對明顯的紅右連接,紅色的右連接違反了紅黑樹定義的第一條規則,因此咱們須要將它進行左旋轉操做,被操做了左旋轉後,元素E的位置會被元素S取代,E元素成爲了S的左子節點,符合了二叉樹的定義,左旋轉的具體代碼:

private Node rotateLeft(Node h) {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = x.left.color;
        x.left.color = RED;
        x.size = h.size;
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }

 

右旋轉

當左邊出現連續的左紅連接時,把左連接放到右邊

 

右旋轉的代碼(右旋轉的代碼和左旋轉幾乎相同把 x.left 換成 x.right 便可)

private Node rotateRight(Node h) {
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = x.right.color;
        x.right.color = RED;
        x.size = h.size;
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }

變色

當左右子節點都是紅色的時候,把顏色進行轉換,具體如圖:

顏色轉換的代碼也很是簡單:

private void flipColors(Node h) {
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
}

理解了以上三種操做的原理,基本也就理解了紅黑樹的原理,有了這三種操做的基本知識,最後咱們開始結合案例來分析紅黑樹插入平衡的全過程

 

爲了便於理解,咱們看一個簡單的例子,下面羅列的三種狀況:

  • 插入最大鍵

  • 插入最小鍵

  • 插入中間鍵

咱們能夠發現,不管插入的數據如何不一樣,經過旋轉,變色操做後最終獲得的結果都是相同的,樹永遠保持平衡,具體能夠看下方的示意圖:

有了上面的理解,咱們能夠分析一組有序數據插入的過程,再結合文字逐步分析紅黑樹是怎麼把它構造爲一顆接近完美平衡的樹

解析:

  1. A首先成爲根節點

  2. C首先插入在A的右邊,A違反了不能出現紅右子節點的規則,進行左旋轉,A成了C的左紅子節點

  3. E首先插入在C的右邊,C違反左右子節點均爲紅色的規則,進行變色,C,A,E變黑(根節點永遠爲黑)

  4. H首先插入在E的右邊,E違反了不能出現紅右子節點的規則,進行左旋轉,E成了H的左紅子節點

  5. L首先插入在H的右邊,H違反左右子節點均爲紅色的規則,進行變色,E,L變黑,H變紅,致使C違反了不能出現紅右子節點的規則,進行左旋轉,C成爲H的左紅子節點(這裏違反2個規則)

  6. M首先插入在L的右邊,L違反了不能出現紅右子節點的規則,進行左旋轉,L成爲M的左紅子節點

  7. P首先插入在M的右邊,M違反左右子節點均爲紅色的規則,進行變色,L,P變黑,M變紅,致使H違反左右子節點均爲紅色的規則,進行變色,H,C,M變黑(這裏違反2個規則)

  8. R首先插入到P的右邊,P違反了不能出現紅右子節點的規則,進行左旋轉,P成爲R的左紅子節點

  9. S首先插入到R的右邊,R違反左右子節點均爲紅色的規則,進行變色,S,P變黑,R變紅,致使M違反了不能出現紅右子節點的規則,進行左旋轉,M成爲R的左紅子節點(這裏違反2個規則)

  10. X首先插入到S的右邊,S違反了不能出現紅右子節點的規則,進行左旋轉,S成爲X的左紅子節點

經過以上證實,就能夠得出結論,和二叉樹不一樣,不管數據的插入順序如何,紅黑樹均可以保證完美平衡

 

理解紅黑樹的背後思想,就能明白只要謹慎的使用簡單的,左旋,右旋,變色這三個操做,就能夠保證紅黑樹的兩種重要的特性 有序性和完美平衡性,由於旋轉和變色都是局部操做,因此無需爲整棵樹的平衡性擔憂,另外紅黑樹的查找徹底和二叉樹相同,不須要額外的平衡,這裏並不打算講紅黑樹的刪除操做,由於紅黑樹的刪除實現複雜,比插入平衡還要複雜的多,要在文章裏講清楚很困難,推薦你們去看看我開篇推薦的經典書籍

 

 

總結

到這裏對於爲何要使用紅黑樹的結論已經很是簡單了,紅黑樹最吸引人的是它的全部操做在 最好 最壞 狀況下均可以保證對數級別的時間複雜度 O(logN),是什麼概念呢,能夠簡單說明對比下:

 

例如要在十億級別的數據量找到一條數據,十億的對數是30,線性表要找到數據須要訪問十億次,而使用紅黑樹的書只須要訪問30次元素就能找到,10億次/30次,差很少是3千萬倍的性能提高,在現代上千億數據的信息海洋裏,只要經過幾十次的比較就能隨意的插入和查找數據,這是多麼了不得的成就呀

 

並且對於二叉樹,無數的實驗和應用都能證實,紅黑樹的操做成本比二叉樹要低 40% 左右(包含旋轉和變色),紅黑樹自從被發現這40年來,一直高效穩定的經過各類應用的考驗,包含須要系統基礎組件和類庫都是用紅黑樹,因此很是值得咱們去學習和掌握它,最後留給你們一個問題,紅黑樹和散列表有什麼區別,散列表查找的時間複雜度是常數級別 O(1),那爲何不少場景咱們不用散列表而用紅黑樹呢?歡迎留言拍磚

 

參考資料

https://algs4.cs.princeton.edu/33balanced/

https://algs4.cs.princeton.edu/33balanced/RedBlackBST.java.html

https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91

https://book.douban.com/subject/10432347/ 

相關文章
相關標籤/搜索