紅黑樹概念、TreeMap的插入以及旋轉的詳細解析(圖解)


前言

網上有很多紅黑樹的插入解析,LZ也看了很多,在看着這些文章結合着源碼看,總感覺沒有get到重點,http://www.cnblogs.com/xrq730/p/6867924.html這篇文章講述得很好,LZ也是借助這篇文章(文中很多概念性的地方都是復制這篇文章),了解了紅黑樹的插入旋轉,只是有一些小問題,沒有講述明白,所以寫了這篇文章,算是一個補充吧,若有不對的地方,歡迎指正,互相學習

紅黑樹基本概念

先來了解一下二叉查找樹的概念:

二叉查找樹

二叉查找樹,也稱有序二叉樹(ordered binary tree),或已排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:

  ❤ 若任意節點的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值

  ❤ 若任意節點的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值

  ❤ 任意節點的左、右子樹也分別為二叉查找樹

  ❤ 沒有鍵值相等的節點(no duplicate nodes)

 因為一棵由n個結點隨機構造的二叉查找樹的高度為logn,所以順理成章,二叉查找樹的一般操作的執行時間為O(lgn)。但二叉查找樹若退化成了一棵具有n個結點的線性鏈后,則這些操作最壞情況運行時間為O(n),如下圖情況:

紅黑樹是一種近似平衡的二叉樹(不是高度的平衡樹),它能夠確保:從根到葉子的最長路徑不多於最短路徑的兩倍長。紅黑樹雖然本質上是一棵二叉查找樹,但它在二叉查找樹的基礎上增加了着色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)

紅黑樹的特點:

  1、根節點與葉節點都是黑色節點,其中葉節點為null節點

  2、不能有兩個連續的紅色節點

  3、從根節點到所有葉子結點上的黑色節點數量相同

上述的特點導致紅黑樹是大致平衡的,保證了紅黑樹能夠以O(log2n)的時間復雜度進行搜索、插入、刪除。

紅黑樹的應用場景

HashMap、LinkedHashMap,以O(1)的時間復雜度進行增刪改查。但缺點是它們的統計性能時間復雜度不是很好,所有的統計必須遍歷所有Entry,因此時間復雜度為O(N)。比如Map的Key有1、2、3、4、5、6、7,我現在要統計:

  1.所有Key比3大的鍵值對有哪些?

  2.Key最小的和Key最大的是哪兩個?

就類似這些操作,HashMap和LinkedHashMap做得比較差,此時我們可以使用TreeMap。TreeMap的Key按照自然順序進行排序或者根據創建映射時提供的Comparator接口進行排序。TreeMap為增、刪、改、查這些操作提供了log(N)的時間開銷,從存儲角度而言,這比HashMap與LinkedHashMap的O(1)時間復雜度要差些;但是在統計性能上,TreeMap同樣可以保證log(N)的時間開銷,這又比HashMap與LinkedHashMap的O(N)時間復雜度好不少。

因此總結而言:如果只需要存儲功能,使用HashMap與LinkedHashMap是一種更好的選擇;如果還需要保證統計性能或者需要對Key按照一定規則進行排序,那么使用TreeMap是一種更好的選擇。

下面展示一張紅黑樹的實例圖:

可以看到根節點到所有NULL LEAF節點(即葉子節點)所經過的黑色節點都是2個。

另外從這張圖上我們還能得到一個結論:紅黑樹並不是高度的平衡樹。所謂平衡樹指的是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,但是我們看:

  · 最左邊的路徑0026-->0017-->0012-->0010-->0003-->NULL LEAF,它的高度為5

  · 最后邊的路徑0026-->0041-->0047-->NULL LEAF,它的高度為3

左右子樹的高度差值為2,因此紅黑樹並不是高度平衡的,它放棄了高度平衡的特性而只追求部分平衡,這種特性降低了插入、刪除時對樹旋轉的要求,從而提升了樹的整體性能。而其他平衡樹比如AVL樹雖然查找性能為性能是O(logn),但是為了維護其平衡特性,可能要在插入、刪除操作時進行多次的旋轉,產生比較大的消耗。

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;
......
}

可以看出,Entry中除了基本的key、value之外,還有左節點、右節點以及父節點,另外還有顏色黑色為true,紅色為false

TreeMap添加數據圖文詳細解析

先從源碼看put方法:

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    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);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

從這段代碼,先總結一下TreeMap添加數據的幾個步驟:

  1. 獲取根節點,根節點為空,產生一個根節點,將其着色為黑色,退出余下流程;
  2. 獲取比較器,如果傳入的Comparator接口不為空,使用傳入的Comparator接口實現類進行比較;如果傳入的Comparator接口為空,將Key強轉為Comparable接口進行比較;
  3. 從根節點開始逐一依照規定的排序算法進行比較,取比較值cmp,如果cmp=0,表示插入的Key已存在;如果cmp>0,取當前節點的右子節點;如果cmp<0,取當前節點的左子節點;
  4. 排除插入的Key已存在的情況,第(3)步的比較一直比較到當前節點t的左子節點或右子節點為null,此時t就是我們尋找到的節點,cmp>0則准備往t的右子節點插入新節點,cmp<0則准備往t的左子節點插入新節點;
  5. new出一個新節點,默認為黑色,根據cmp的值向t的左邊或者右邊進行插入;
  6. 插入之后進行修復,包括左旋、右旋、重新着色這些操作,讓樹保持平衡性

1~5步都比較簡單,看代碼就基本能理解,紅黑樹最核心的應當是第6步插入數據之后進行的修復工作,對應的Java代碼是TreeMap中的fixAfterInsertion方法,下面看一下put每個數據之后TreeMap都做了什么操作,借此來理清TreeMap的實現原理。

fixAfterInsertion

 1 private void fixAfterInsertion(Entry<K,V> x) {
 2     x.color = RED;
 3 
 4     while (x != null && x != root && x.parent.color == RED) {
 5         if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
 6             Entry<K,V> y = rightOf(parentOf(parentOf(x)));
 7             if (colorOf(y) == RED) {
 8                 setColor(parentOf(x), BLACK);
 9                 setColor(y, BLACK);
10                 setColor(parentOf(parentOf(x)), RED);
11                 x = parentOf(parentOf(x));
12             } else {
13                 if (x == rightOf(parentOf(x))) {
14                     x = parentOf(x);
15                     rotateLeft(x);
16                 }
17                 setColor(parentOf(x), BLACK);
18                 setColor(parentOf(parentOf(x)), RED);
19                 rotateRight(parentOf(parentOf(x)));
20             }
21         } else {
22             Entry<K,V> y = leftOf(parentOf(parentOf(x)));
23             if (colorOf(y) == RED) {
24                 setColor(parentOf(x), BLACK);
25                 setColor(y, BLACK);
26                 setColor(parentOf(parentOf(x)), RED);
27                 x = parentOf(parentOf(x));
28             } else {
29                 if (x == leftOf(parentOf(x))) {
30                     x = parentOf(x);
31                     rotateRight(x);
32                 }
33                 setColor(parentOf(x), BLACK);
34                 setColor(parentOf(parentOf(x)), RED);
35                 rotateLeft(parentOf(parentOf(x)));
36             }
37         }
38     }
39     root.color = BLACK;
40 }

上述代碼用流程圖來描述,便於理解:

在總結上述流程之前先說明幾個概念:

上圖從左到右依次是:左子樹外側插入、左子樹內側插入、右子樹內側插入、右子樹外側插入(這個就不解釋了,思考一下就明白了)

再對上述的流程圖進行總結(TreeMap的修復旋轉):

在插入判斷之前,不論是左子樹還是右子樹都需要先判斷x的叔父節點是否為紅色如果為否才有下面的插入判斷:

if

  如果是叔父節點是紅色則表示:x的父節點和叔父節點都為紅色,則需要做

    1.將x的父節點、叔父節點着為黑色

    2.將x的祖父節點着為紅色

    3.將root着為黑色

    4.如果上述兩步昨晚還存在兩個紅色節點連在一起,那么將x賦值為其祖父節點用於while條件重新判斷,即從新開始判斷(上述的if)

else

  (1)左子樹的內側插入:

    1.將x賦值為x的父節點

    2.對x(實際上就是對原始x的父節點)進行一次左旋

    3.對x的父節點着為黑色,對x的祖父節點着為紅色 

    4.對x祖父節點進行一次右旋

  (2)左子樹的外側插入:

    1.x的父節點着為黑色

    2.x的祖父節點着為紅色

    3.對x的祖父節點進行一次右旋

  (3)右子樹的內側插入:

    1.將x賦值為x的父節點

    2.對x(實際上就是對x的父節點)進行一次右旋

    3.x的父節點着為黑色,x的祖父節點着為紅色

    4.對x的祖父節點進行一次左旋

  (4)右子樹的外側插入:

    1.x的父節點着為黑色

    2.x的祖父節點着為紅色

    3.對x的祖父節點進行一次左旋

接下來舉個例子,向TreeMap中插入數據,利用圖文來詳細的解析插入操作:

     @Test
        public void testTreeMap() {
            TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
            treeMap.put(10, "10");
            treeMap.put(90, "90");
            treeMap.put(15, "15");
            treeMap.put(65, "65");
            treeMap.put(20, "20");
            treeMap.put(55, "55");
            treeMap.put(30, "30");
            treeMap.put(40, "40");

            for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            }
        }

下面就通過圖文的方式來模擬向TreeMap中添加數據:

put(10,"10")

首先是put(10,"10"),由於此時TreeMap中沒有任何節點,所以10位根節點且為黑色,所以put之后的數據結構為:

                

put(90,"90")

90比10大,且新加入的元素都會被着為紅色,所以加入90后不會觸發fixAfterInsertion內的判斷旋轉,只是將其着為紅色,就修正結束了,放入90后的數據結構為:

put(15,"15")

put(15,"15")由於15比10大,比90小,所以15應該插在90的左子節點上,默認的節點都是紅色,所以插入15節點都的數據結構是:

顯然,違反了上述紅黑樹的第二個特點:不能有兩個連續的紅色節點,此時15的插入首先只有父節點,沒有叔父節點,所以走else,再判斷為右子樹的內側插入,所以按照上述else中的(3)來執行旋轉和着色:

    1.將x賦值為x的父節點

    2.對x(實際上就是對x的父節點)進行一次右旋

    3.x的父節點着為黑色,x的祖父節點着為紅色(此時x為90)

 

    4.對x的祖父節點進行一次左旋

 

經過上述四個步驟之后,發現現在的數據結構符合紅黑樹的三個特點,即修正結束。

put(65,"65")

65比15大,比90小,故65應在90的左子節點上,默認新元素都為紅色,插入后為:

發現違反了紅黑樹的第二條特點:不能有兩個連續的紅色節點,即需要調整。

此時發現:節點65的父節點和叔父節點都是紅色,所以走if判斷。

  如果是叔父節點是紅色則表示:x的父節點和叔父節點都為紅色,則需要做

    1.將x的父節點、叔父節點着為黑色

    2.將x的祖父節點着為紅色

        3.這里結束前有句:root.color = BLACK;所以將root = 15 着為黑色

發現符合紅黑樹的特點,修正完畢

put(20,"20")

由於20比15大,比65小,故20應該放在65的左子節點上,放入后的數據結構為:

發現違反了紅黑樹的第二條特點:不能有兩個連續的紅色節點,再判斷發現這是左子樹的外側插入:

    1.x的父節點着為黑色

    2.x的祖父節點着為紅色

    3.對x的祖父節點進行一次右旋

 

發現符合紅黑樹的特點,修正結束

put(55,"55")

55比15大,比20大,所以55節點應該在節點20的右子節點,數據結構為:

發現違反了紅黑樹的第二條特點:不能有兩個連續的紅色節點,再判斷發現55節點的叔父節點和父節點都為紅色:

這里開始就不一步一步畫出來了,直接一次調整到位:

    1.將x的父節點、叔父節點着為黑色

    2.將x的祖父節點着為紅色

    3.將root着為黑色

一次調整后發現符合紅黑樹的特點,修正結束

put(30,"30")

30比15大,比55小,所以30節點應該放在55節點的左子節點上,數據結構為:

發現違反了紅黑樹的第二條特點:不能有兩個連續的紅色節點,再判斷發現這是右子樹的內側插入:

    1.將x賦值為x的父節點

    2.對x(實際上就是對x的父節點)進行一次右旋

    3.x的父節點着為黑色,x的祖父節點着為紅色

    4.對x的祖父節點進行一次左旋

put(40,"40")

40比15大,比30大,比55小,所以40節點應該在55節點的左子節點上,數據結構為:

發現違反了紅黑樹的第二條特點:不能有兩個連續的紅色節點,再判斷發現55節點的叔父節點和父節點都為紅色:

    1.將x的父節點、叔父節點着為黑色

    2.將x的祖父節點着為紅色

    3.將root着為黑色

 

第一次調整結束后發現,30,65兩個節點是紅色相連所以還是需要再一次的調整:這次將30看做是新插入的元素,這樣15--》65--》30,看做是30節點的右子樹內側插入:

    1.將x賦值為x的父節點

    2.對x(實際上就是對x的父節點)進行一次右旋

    3.x的父節點着為黑色,x的祖父節點着為紅色(此時x為65)

    4.對x的祖父節點進行一次左旋

這次修正后,重新恢復紅黑樹的特性,修正結束。

上述就是添加旋轉的過程,其實多動手畫,畫幾遍就理解了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM