我畫了近百張圖來理解紅黑樹

文章已同步發表於微信公衆號JasonGaoH,我畫了近百張圖來理解紅黑樹,文章略有修改。html

以前在公司組內分享了紅黑樹的工做原理,今天把它整理下發出來,但願能對你們有所幫助,對本身也算是一個知識點的總結。java

這篇文章算是我寫博客寫公衆號以來畫圖最多的一篇文章了,沒有之一,我但願儘量多地用圖片來形象地描述紅黑樹的各類操做的先後變換原理,幫助你們來理解紅黑樹的工做原理,下面,多圖預警開始了。git

在講紅黑樹以前,咱們首先來了解下下面幾個概念:二叉樹,排序二叉樹以及平衡二叉樹。github

二叉樹

二叉樹指的是每一個節點最多隻能有兩個字數的有序樹。一般左邊的子樹稱爲左子樹 ,右邊的子樹稱爲右子樹 。這裏說的有序樹強調的是二叉樹的左子樹和右子樹的次序不能隨意顛倒。bash

二叉樹簡單的示意圖以下:微信

代碼定義:網絡

class Node {
    T data;
    Node left;
    Node right;
}
複製代碼

排序二叉樹

所謂排序二叉樹,顧名思義,排序二叉樹是有順序的,它是一種特殊結構的二叉樹,咱們能夠對樹中全部節點進行排序和檢索。性能

性質ui

  • 若它的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;
  • 若她的右子樹不空,則右子樹上全部節點的值均大於它的根節點的值;
  • 具備遞歸性,排序二叉樹的左子樹、右子樹也是排序二叉樹。

排序二叉樹簡單示意圖:spa

排序二叉樹

排序二叉樹退化成鏈表

排序二叉樹的左子樹上全部節點的值小於根節點的值,右子樹上全部節點的值大於根節點的值,當咱們插入一組元素正好是有序的時候,這時會讓排序二叉樹退化成鏈表。

正常狀況下,排序二叉樹是以下圖這樣的:

可是,當插入的一組元素正好是有序的時候,排序二叉樹就變成了下邊這樣了,就變成了普通的鏈表結構,以下圖所示:

正常狀況下的排序二叉樹檢索效率相似於二分查找,二分查找的時間複雜度爲 O(log n),可是若是排序二叉樹退化成鏈表結構,那麼檢索效率就變成了線性的 O(n) 的,這樣相對於 O(log n) 來講,檢索效率確定是要差很多的。

思考,二分查找和正常的排序二叉樹的時間複雜度都是 O(log n),那麼爲何是O(log n) ?

關於 O(log n) 的分析下面這篇文章講解的很是好,感興趣的能夠看下這篇文章 二分查找的時間複雜度,文章是拿二分查找來舉例的,二分查找和平衡二叉樹的時間複雜度是同樣的,理解了二分查找的時間複雜度,再來理解平衡二叉樹就不難了,這裏就不贅述了。

繼續回到咱們的主題上,爲了解決排序二叉樹在特殊狀況下會退化成鏈表的問題(鏈表的檢索效率是 O(n) 相對正常二叉樹來講要差很多),因此有人發明了平衡二叉樹紅黑樹相似的平衡樹。

平衡二叉樹

平衡二叉數又被稱爲 AVL 樹,AVL 樹的名字來源於它的發明做者 G.M. Adelson-Velsky 和 E.M. Landis,取自兩人名字的首字母。

官方定義:它或者是一顆空樹,或者具備如下性質的排序二叉樹:它的左子樹和右子樹的深度之差(平衡因子)的絕對值不超過1,且它的左子樹和右子樹都是一顆平衡二叉樹。

兩個條件:

  • 平衡二叉樹必須是排序二叉樹,也就是說平衡二叉樹他的左子樹全部節點的值必須小於根節點的值,它的右子樹上全部節點的值必須大於它的根節點的值。
  • 左子樹和右子樹的深度之差的絕對值不超過1。

紅黑樹

講了這麼多概念,接下來主角紅黑樹終於要上場了。

爲何有紅黑樹?

其實紅黑樹和上面的平衡二叉樹相似,本質上都是爲了解決排序二叉樹在極端狀況下退化成鏈表致使檢索效率大大下降的問題,紅黑樹最先是由 Rudolf Bayer 於 1972 年發明的。

紅黑樹首先確定是一個排序二叉樹,它在每一個節點上增長了一個存儲位來表示節點的顏色,能夠是 RED 或 BLACK 。

Java 中實現紅黑樹大概結構圖以下所示:

紅黑樹的特性

  • 性質1:每一個節點要麼是紅色,要麼是黑色。
  • 性質2:根節點永遠是黑色的。
  • 性質3:全部的葉子節點都是空節點(即null),而且是黑色的。
  • 性質4:每一個紅色節點的兩個子節點都是黑色。(從每一個葉子到根的路徑上不會有兩個連續的紅色節點。)
  • 性質5:從任一節點到其子樹中每一個葉子節點的路徑都包含相同數量的黑色節點。

針對上面的 5 種性質,咱們簡單理解下,對於性質 1 和性質 2 ,至關因而對紅黑樹每一個節點的約束,根節點是黑色,其餘的節點要麼是紅色,要麼是黑色。

對於性質 3 中指定紅黑樹的每一個葉子節點都是空節點,並且葉子節點都是黑色,但 Java 實現的紅黑樹會使用 null 來表明空節點,所以咱們在遍歷 Java裏的紅黑樹的時候會看不到葉子節點,而看到的是每一個葉子節點都是紅色的,這一點須要注意。

對於性質 5,這裏咱們須要注意的是,這裏的描述是從任一節點,從任一節點到它的子樹的每一個葉子節點黑色節點的數量都是相同的,這個數量被稱爲這個節點的黑高。

若是咱們從根節點出發到每一個葉子節點的路徑都包含相同數量的黑色節點,這個黑色節點的數量被稱爲樹的黑色高度。樹的黑色高度和節點的黑色高度是不同的,這裏要注意區分。

其實到這裏有人可能會問了,紅黑樹的性質說了一大堆,那是否是說只要保證紅黑樹的節點是紅黑交替就能保證樹是平衡的呢?

其實不是這樣的,咱們能夠看來看下面這張圖:

左邊的子樹都是黑色節點,可是這個紅黑樹依然是平衡的,5 條性質它都知足。

這個樹的黑色高度爲 3,從根節點到葉子節點的最短路徑長度是 2,該路徑上全是黑色節點,包括葉子節點,從根節點到葉子節點最長路徑爲 4,每一個黑色節點之間會插入紅色節點。

經過上面的性質 4 和性質 5,其實上保證了沒有任何一條路徑會比其餘路徑長出兩倍,因此這樣的紅黑樹是平衡的。

其實這算是一個推論,紅黑樹在最差狀況下,最長的路徑都不會比最短的路徑長出兩倍。其實紅黑樹並非真正的平衡二叉樹,它只能保證大體是平衡的,由於紅黑樹的高度不會無限增高,在實際應用用,紅黑樹的統計性能要高於平衡二叉樹,但極端性能略差。

紅黑樹的插入

想要完全理解紅黑樹,除了上面說到的理解紅黑樹的性質之外,就是理解紅黑樹的插入操做了。

紅黑樹的插入和普通排序二叉樹的插入基本一致,排序二叉樹的要求是左子樹上的全部節點都要比根節點小,右子樹上的全部節點都要比跟節點大,當插入一個新的節點的時候,首先要找到當前要插入的節點適合放在排序二叉樹哪一個位置,而後插入當前節點便可。紅黑樹和排序二叉樹不一樣的是,紅黑樹須要在插入節點調整樹的結構來讓樹保持平衡。

通常狀況下,紅黑樹中新插入的節點都是紅色的,那麼,爲何說新加入到紅黑樹中的節點要是紅色的呢?

這個問題能夠這樣理解,咱們從性質5中知道,當前紅黑樹中從根節點到每一個葉子節點的黑色節點數量是同樣的,此時假如新的黑色節點的話,必然破壞規則,但加入紅色節點卻不必定,除非其父節點就是紅色節點,所以加入紅色節點,破壞規則的可能性小一些。

接下來咱們重點來說紅黑樹插入新節點後是如何保持平衡的。

給定下面這樣一顆紅黑樹:

當咱們插入值爲66的節點的時候,示意圖以下:

很明顯,這個時候結構依然遵循着上述5大特性,無需啓動自動平衡機制調整節點平衡狀態。

若是再向裏面插入值爲51的節點呢,這個時候紅黑樹變成了這樣。

這樣的結構其實是不知足性質4的,紅色兩個子節點必須是黑色的,而這裏49這個紅色節點如今有個51的紅色節點與其相連。

這個時候咱們須要調整這個樹的結構來保證紅黑樹的平衡。

首先嚐試將49這個節點設置爲黑色,以下示意圖。

這個時候咱們發現黑高是不對的,其中 60-56-45-49-51-null 這條路徑有 4 個黑節點,其餘路徑的黑色節點是 3 個。

接着調整紅黑樹,咱們再次嘗試把45這個節點設置爲紅色的,以下圖所示:

這個時候咱們發現問題又來了,56-45-43 都是紅色節點的,出現了紅色節點相連的問題。

因而咱們須要再把 56 和 43 設置爲黑色的,以下圖所示。

因而咱們把 68 這個紅色節點設置爲黑色的。

對於這種紅黑樹插入節點的狀況下,咱們能夠只須要經過變色就能夠保持樹的平衡了。可是並非每次都是這麼幸運的,當變色行不通的時候,咱們須要考慮另外一個手段就是旋轉了。

例以下面這種狀況,一樣仍是拿這顆紅黑樹舉例。

如今這顆紅黑樹,咱們如今插入節點65。

咱們嘗試把 66 這個節點設置爲黑色,以下圖所示。

這樣操做以後黑高又出現不一致的狀況了,60-68-64-null 有 3 個黑色節點,而60-68-64-66-null 這條路徑有 4 個黑色節點,這樣的結構是不平衡的。

或者咱們把 68 設置爲黑色,把 64 設置爲紅色,以下圖所示:

可是,一樣的問題,上面這顆紅黑樹的黑色高度仍是不一致,60-68-64-null 和 60-68-64-66-null 這兩條路徑黑色高度仍是不一致。

這種狀況若是隻經過變色的狀況是不能保持紅黑樹的平衡的。

紅黑樹的旋轉

接下來咱們講講紅黑樹的旋轉,旋轉分爲左旋和右旋。

左旋

文字描述:逆時針旋轉兩個節點,讓一個節點被其右子節點取代,而該節點成爲右子節點的左子節點。

文字描述太抽象,接下來看下圖片展現。

首先斷開節點PL與右子節點G的關係,同時將其右子節點的引用指向節點C2;而後斷開節點G與左子節點C2的關係,同時將G的左子節點的應用指向節點PL。

接下來再放下 gif 圖,但願能幫助你們更好地理解左旋,圖片來自網絡。

右旋

文字描述:順時針旋轉兩個節點,讓一個節點被其左子節點取代,而該節點成爲左子節點的右子節點。

右旋的圖片展現:

首先斷開節點G與左子節點PL的關係,同時將其左子節點的引用指向節點C2;而後斷開節點PL與右子節點C2的關係,同時將PL的右子節點的應用指向節點G。

右旋的gif展現(圖片來自網絡):

介紹完了左旋和右旋基本操做,咱們來詳細介紹下紅黑樹的幾種旋轉場景。

左左節點旋轉(插入節點的父節點是左節點,插入節點也是左節點)

以下圖所示的紅黑樹,咱們插入節點是65。

操做步驟以下能夠圍繞祖父節點 69 右旋,再結合變色,步驟以下所示:

左右節點旋轉(插入節點的父節點是左節點,插入節點是右節點)

仍是上面這顆紅黑樹,咱們再插入節點 67。

這種狀況咱們能夠這樣操做,先圍繞父節點 66 左旋,而後再圍繞祖父節點 69 右旋,最後再將 67 設置爲黑色,把 69 設置爲紅色,以下圖所示。

右左節點旋轉(插入節點的父節點是右節點,插入節點左節點)

以下圖這種狀況,咱們要插入節點68。

這種狀況,咱們能夠先圍繞父節點 69 右旋,接着再圍繞祖父節點 66 左旋,最後把 68 節點設置爲黑色,把 66 設置爲紅色,咱們的具體操做步驟以下所示。

右右節點旋轉(插入節點的父節點是右節點,插入節點也是右節點)

仍是來上面的圖來舉例,咱們在這顆紅黑樹上插入節點 70 。

咱們能夠這樣操做圍繞祖父節點 66 左旋,再把旋轉後的根節點 69 設置爲黑色,把 66 這個節點設置爲紅色。具體能夠參看下圖:

紅黑樹在 Java 中的實現

Java 中的紅黑樹實現類是 TreeMap ,接下來咱們嘗試從源碼角度來逐行解釋 TreeMap 這一套機制是如何運做的。

// TreeMap中使用Entry來描述每一個節點
 static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
        ...
 }
複製代碼

TreeMap 的put方法。

public V put(K key, V value) {
        //先以t保存鏈表的root節點
        Entry<K,V> t = root;
        //若是t=null,代表是一個空鏈表,即該TreeMap裏沒有任何Entry做爲root
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            //將新的key-value建立一個Entry,並將該Entry做爲root
            root = new Entry<>(key, value, null);
            size = 1;
            //記錄修改次數加1
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        //若是比較器cpr不爲null,即代表採用定製排序
        if (cpr != null) {
            do {
                //使用parent上次循環後的t所引用的Entry
                parent = t;
                 //將新插入的key和t的key進行比較
                cmp = cpr.compare(key, t.key);
                //若是新插入的key小於t的key,t等於t的左邊節點
                if (cmp < 0)
                    t = t.left;
                //若是新插入的key大於t的key,t等於t的右邊節點    
                else if (cmp > 0)
                    t = t.right;
                else
                //若是兩個key相等,新value覆蓋原有的value,並返回原有的value
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //將新插入的節點做爲parent節點的子節點
        Entry<K,V> e = new Entry<>(key, value, parent);
        //若是新插入key小於parent的key,則e做爲parent的左子節點
        if (cmp < 0)
            parent.left = e;
        //若是新插入key小於parent的key,則e做爲parent的右子節點
        else
            parent.right = e;
        //修復紅黑樹
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
複製代碼
//插入節點後修復紅黑樹
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;

    //直到x節點的父節點不是根,且x的父節點是紅色
    while (x != null && x != root && x.parent.color == RED) {
        //若是x的父節點是其父節點的左子節點
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //獲取x的父節點的兄弟節點
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //若是x的父節點的兄弟節點是紅色
            if (colorOf(y) == RED) {     
                //將x的父節點設置爲黑色
                setColor(parentOf(x), BLACK);
                //將x的父節點的兄弟節點設置爲黑色
                setColor(y, BLACK);
                //將x的父節點的父節點設爲紅色
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            }
            //若是x的父節點的兄弟節點是黑色
            else {   
                //TODO 對應狀況第二種,左右節點旋轉
                //若是x是其父節點的右子節點
                if (x == rightOf(parentOf(x))) {
                    //將x的父節點設爲x
                    x = parentOf(x);
                    //右旋轉
                    rotateLeft(x);
                }
                //把x的父節點設置爲黑色
                setColor(parentOf(x), BLACK);
                //把x的父節點父節點設爲紅色
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        }
        //若是x的父節點是其父節點的右子節點
        else {
            //獲取x的父節點的兄弟節點
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //只着色的狀況對應的是最開始例子,沒有旋轉操做,可是要對應屢次變換
            //若是x的父節點的兄弟節點是紅色  
            if (colorOf(y) == RED) {
                //將x的父節點設置爲黑色
                setColor(parentOf(x), BLACK);
                //將x的父節點的兄弟節點設爲黑色
                setColor(y, BLACK);
                //將X的父節點的父節點(G)設置紅色
                setColor(parentOf(parentOf(x)), RED);
                //將x設爲x的父節點的節點
                x = parentOf(parentOf(x));
            }
            //若是x的父節點的兄弟節點是黑色
            else {
                //若是x是其父節點的左子節點
                if (x == leftOf(parentOf(x))) {
                    //將x的父節點設爲x
                    x = parentOf(x);
                    //右旋轉
                    rotateRight(x);
                }
                //將x的父節點設爲黑色
                setColor(parentOf(x), BLACK);
                //把x的父節點的父節點設爲紅色
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //將根節點強制設置爲黑色
    root.color = BLACK;
}
複製代碼

TreeMap的插入節點和普通的排序二叉樹沒啥區別,惟一不一樣的是,在TreeMap 插入節點後會調用方法fixAfterInsertion(e)來從新調整紅黑樹的結構來讓紅黑樹保持平衡。

咱們重點關注下紅黑樹的fixAfterInsertion(e)方法,接下來咱們來分別介紹兩種場景來演示fixAfterInsertion(e)方法的執行流程。

第一種場景:只需變色便可平衡

一樣是拿這顆紅黑樹舉例,如今咱們插入節點 51。

當咱們須要插入節點51的時候,這個時候TreeMap 的 put 方法執行後會獲得下面這張圖。

接着調用fixAfterInsertion(e)方法,以下代碼流程所示。

當第一次進入循環後,執行後會獲得下面的紅黑樹結構。

在把 x 從新賦值後,從新進入 while 循環,此時的 x 節點爲 45 。

執行上述流程後,獲得下面所示的紅黑樹結構。

這個時候x被從新賦值爲60,由於60是根節點,因此會退出 while 循環。在退出循序後,會再次把根節點設置爲黑色,獲得最終的結構以下圖所示。

最後通過兩次執行while循環後,咱們的紅黑樹會調整成如今這樣的結構,這樣的紅黑樹結構是平衡的,因此路徑的黑高一致,而且沒有紅色節點相連的狀況。

第二種場景 旋轉搭配變色來保持平衡

接下來咱們再來演示第二種場景,須要結合變色和旋轉一塊兒來保持平衡。

給定下面這樣一顆紅黑樹:

如今咱們插入節點66,獲得以下樹結構。

一樣地,咱們進入fixAfterInsertion(e)方法。

最終咱們獲得的紅黑樹結構以下圖所示:

調整成這樣的結構咱們的紅黑樹又再次保持平衡了。

演示 TreeMap 的流程就拿這兩種場景舉例了,其餘的就不一一舉例了。

紅黑樹的刪除

由於以前的分享只整理了紅黑樹的插入部分,原本想着紅黑樹的刪除就不整理了,有人跟我反饋說紅黑樹的刪除相對更復雜,因而索性仍是把紅黑樹的刪除再整理下。

刪除相對插入來講,的確是要複雜一點,可是複雜的地方是由於在刪除節點的這個操做狀況有不少種,可是插入不同,插入節點的時候實際上這個節點的位置是肯定的,在節點插入成功後只須要調整紅黑樹的平衡就能夠了。

可是刪除不同的是,刪除節點的時候咱們不能簡單地把這個節點設置爲null,由於若是這個節點有子節點的狀況下,不能簡單地把當前刪除的節點設置爲null,這個被刪除的節點的位置須要有新的節點來填補。這樣一來,須要分多種狀況來處理了。

刪除節點是根節點

直接刪除根節點便可。

刪掉節點的左子節點和右子節點都是爲空

直接刪除當前節點便可。

刪除節點有一個子節點不爲空

這個時候須要使用子節點來代替當前須要刪除的節點,而後再把子節點刪除便可。

給定下面這棵樹,當咱們須要刪除節點69的時候。

首先用子節點代替當前待刪除節點,而後再把子節點刪除。

最終的紅黑樹結構以下面所示,這個結構的紅黑樹咱們是不須要經過變色+旋轉來保持紅黑樹的平衡了,由於將子節點刪除後樹已是平衡的了。

還有一種場景是當咱們待刪除節點是黑色的,黑色的節點被刪除後,樹的黑高就會出現不一致的狀況,這個時候就須要從新調整結構。

仍是拿上面這顆刪除節點後的紅黑樹舉例,咱們如今須要刪除節點67。

由於67 這個節點的兩個子節點都是null,因此直接刪除,獲得以下圖所示結構:

這個時候咱們樹的黑高是不一致的,左邊黑高是3,右邊是2,因此咱們須要把64節點設置爲紅色來保持平衡。

刪除節點兩個子節點都不爲空

刪除節點兩個子節點都不爲空的狀況下,跟上面有一個節點不爲空的狀況下也是有點相似,一樣是須要找能替代當前節點的節點,找到後,把能替代刪除節點值複製過來,而後再把替代節點刪除掉。

  • 先找到替代節點,也就是前驅節點或者後繼節點
  • 而後把前驅節點或者後繼節點複製到當前待刪除節點的位置,而後在刪除前驅節點或者後繼節點。

那麼什麼叫作前驅,什麼叫作後繼呢? 前驅是左子樹中最大的節點,後繼則是右子樹中最小的節點。

前驅或者後繼都是最接近當前節點的節點,當咱們須要刪除當前節點的時候,也就是找到能替代當前節點的節點,可以替代當前節點確定是最接近當前節點。

在當前刪除節點兩個子節點不爲空的場景下,咱們須要再進行細分,主要分爲如下三種狀況。

第一種,前驅節點爲黑色節點,同時有一個非空節點

以下面這樣一棵樹,咱們須要刪除節點64:

首先找到前驅節點,把前驅節點複製到當前節點:

接着刪除前驅節點。

這個時候63和60這個節點都是紅色的,咱們嘗試把60這個節點設置爲紅色便可使整個紅黑樹達到平衡。

第二種,前驅節點爲黑色節點,同時子節點都爲空

前驅節點是黑色的,子節點都爲空,這個時候操做步驟與上面基本相似。

以下操做步驟:

由於要刪除節點64,接着找到前驅節點63,把63節點複製到當前位置,而後將前驅節點63刪除掉,變色後出現黑高不一致的狀況下,最後把63節點設置爲黑色,把65節點設置爲紅色,這樣就能保證紅黑樹的平衡。

第三種,前驅節點爲紅色節點,同時子節點都爲空

給定下面這顆紅黑樹,咱們須要刪除節點64的時候。

一樣地,咱們找到64的前驅節點63,接着把63賦值到64這個位置。

而後刪除前驅節點。

刪除節點後不須要變色也不須要旋轉便可保持樹的平衡。

終於把紅黑樹的基本原理部分寫完了,用了不少示意圖,這篇文章是在以前分享的 ppt 上再整理出來,我以爲本身應該算是把基本操做講明白了,整理這篇文章前先後後用了近一週左右,由於平時上班,基本上只有週末有時間纔有時間整理,若有問題請留言討論。

若是您以爲寫得還能夠,請您幫忙點個贊,您的點贊真的是對我最大的支持,也是我能繼續寫下去的動力,感謝。

原文連接

文章中不少參考了下面文章的一些示意圖,很是感謝如下文章。

What does the time complexity O(log n) actually mean?

Java提升篇--TreeMap

關於紅黑樹(R-B tree)原理,看這篇如何

相關文章
相關標籤/搜索