通過分析 JDK 源代碼研究 TreeMap 紅黑樹算法實現

TreeMap 的實現就是紅黑樹數據結構,也就說是一棵自平衡的排序二叉樹,這樣就可以保證當需要快速檢索指定節點。

TreeSet 和 TreeMap 的關係

爲了讓大家瞭解 TreeMap 和 TreeSet 之間的關係,下面先看 TreeSet 類的部分源代碼:

 public class TreeSet<E> extends AbstractSet<E> 
    implements NavigableSet<E>, Cloneable, java.io.Serializable 
 { 
    // 使用 NavigableMap 的 key 來保存 Set 集合的元素
    private transient NavigableMap<E,Object> m; 
    // 使用一個 PRESENT 作爲 Map 集合的所有 value。
    private static final Object PRESENT = new Object(); 
    // 包訪問權限的構造器,以指定的 NavigableMap 對象創建 Set 集合
    TreeSet(NavigableMap<E,Object> m) 
    { 
        this.m = m; 
    } 
    public TreeSet()                                      // ①
    { 
        // 以自然排序方式創建一個新的 TreeMap,
        // 根據該 TreeSet 創建一個 TreeSet,
        // 使用該 TreeMap 的 key 來保存 Set 集合的元素
        this(new TreeMap<E,Object>()); 
    } 
    public TreeSet(Comparator<? super E> comparator)     // ②
    { 
        // 以定製排序方式創建一個新的 TreeMap,
        // 根據該 TreeSet 創建一個 TreeSet,
        // 使用該 TreeMap 的 key 來保存 Set 集合的元素
        this(new TreeMap<E,Object>(comparator)); 
    } 
    public TreeSet(Collection<? extends E> c) 
    { 
        // 調用①號構造器創建一個 TreeSet,底層以 TreeMap 保存集合元素
        this(); 
        // 向 TreeSet 中添加 Collection 集合 c 裏的所有元素
        addAll(c); 
    } 
    public TreeSet(SortedSet<E> s) 
    { 
        // 調用②號構造器創建一個 TreeSet,底層以 TreeMap 保存集合元素
        this(s.comparator()); 
        // 向 TreeSet 中添加 SortedSet 集合 s 裏的所有元素
        addAll(s); 
    } 
    //TreeSet 的其他方法都只是直接調用 TreeMap 的方法來提供實現
    ... 
    public boolean addAll(Collection<? extends E> c) 
    { 
        if (m.size() == 0 && c.size() > 0 && 
            c instanceof SortedSet && 
            m instanceof TreeMap) 
        { 
            // 把 c 集合強制轉換爲 SortedSet 集合
            SortedSet<? extends E> set = (SortedSet<? extends E>) c; 
            // 把 m 集合強制轉換爲 TreeMap 集合
            TreeMap<E,Object> map = (TreeMap<E, Object>) m; 
            Comparator<? super E> cc = (Comparator<? super E>) set.comparator(); 
            Comparator<? super E> mc = map.comparator(); 
            // 如果 cc 和 mc 兩個 Comparator 相等
            if (cc == mc || (cc != null && cc.equals(mc))) 
            { 
                // 把 Collection 中所有元素添加成 TreeMap 集合的 key 
                map.addAllForTreeSet(set, PRESENT); 
                return true; 
            } 
        } 
        // 直接調用父類的 addAll() 方法來實現
        return super.addAll(c); 
    } 
    ... 
 }

從上面代碼可以看出,TreeSet 的 ① 號、② 號構造器的都是新建一個 TreeMap 作爲實際存儲 Set 元素的容器,而另外 2 個構造器則分別依賴於 ① 號和 ② 號構造器,由此可見,TreeSet 底層實際使用的存儲容器就是 TreeMap。

與 HashSet 完全類似的是,TreeSet 裏絕大部分方法都是直接調用 TreeMap 的方法來實現的,這一點讀者可以自行參閱 TreeSet 的源代碼,此處就不再給出了。

對於 TreeMap 而言,它採用一種被稱爲“紅黑樹”的排序二叉樹來保存 Map 中每個 Entry —— 每個 Entry 都被當成“紅黑樹”的一個節點對待。例如對於如下程序而言:

 public class TreeMapTest 
 { 
    public static void main(String[] args) 
    { 
        TreeMap<String , Double> map = 
            new TreeMap<String , Double>(); 
        map.put("ccc" , 89.0); 
        map.put("aaa" , 80.0); 
        map.put("zzz" , 80.0); 
        map.put("bbb" , 89.0); 
        System.out.println(map); 
    } 
 }

當程序執行 map.put("ccc" , 89.0); 時,系統將直接把 "ccc"-89.0 這個 Entry 放入 Map 中,這個 Entry 就是該“紅黑樹”的根節點。接着程序執行 map.put("aaa" , 80.0); 時,程序會將 "aaa"-80.0 作爲新節點添加到已有的紅黑樹中。

以後每向 TreeMap 中放入一個 key-value 對,系統都需要將該 Entry 當成一個新節點,添加成已有紅黑樹中,通過這種方式就可保證 TreeMap 中所有 key 總是由小到大地排列。例如我們輸出上面程序,將看到如下結果(所有 key 由小到大地排列):

 {aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0}

TreeMap 的添加節點

紅黑樹

紅黑樹是一種自平衡排序二叉樹,樹中每個節點的值,都大於或等於在它的左子樹中的所有節點的值,並且小於或等於在它的右子樹中的所有節點的值,這確保紅黑樹運行時可以快速地在樹中查找和定位的所需節點。

對於 TreeMap 而言,由於它底層採用一棵“紅黑樹”來保存集合中的 Entry,這意味這 TreeMap 添加元素、取出元素的性能都比 HashMap 低:當 TreeMap 添加元素時,需要通過循環找到新增 Entry 的插入位置,因此比較耗性能;當從 TreeMap 中取出元素時,需要通過循環才能找到合適的 Entry,也比較耗性能。但 TreeMap、TreeSet 比 HashMap、HashSet 的優勢在於:TreeMap 中的所有 Entry 總是按 key 根據指定排序規則保持有序狀態,TreeSet 中所有元素總是根據指定排序規則保持有序狀態。

爲了理解 TreeMap 的底層實現,必須先介紹排序二叉樹和紅黑樹這兩種數據結構。其中紅黑樹又是一種特殊的排序二叉樹。

排序二叉樹是一種特殊結構的二叉樹,可以非常方便地對樹中所有節點進行排序和檢索。

排序二叉樹要麼是一棵空二叉樹,要麼是具有下列性質的二叉樹:

  • 若它的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  • 若它的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
  • 它的左、右子樹也分別爲排序二叉樹。

圖 1 顯示了一棵排序二叉樹:

圖 1. 排序二叉樹
圖 1. 排序二叉樹

對排序二叉樹,若按中序遍歷就可以得到由小到大的有序序列。如圖 1 所示二叉樹,中序遍歷得:

{2,3,4,8,9,9,10,13,15,18}

創建排序二叉樹的步驟,也就是不斷地向排序二叉樹添加節點的過程,向排序二叉樹添加節點的步驟如下:

  1. 以根節點當前節點開始搜索。
  2. 拿新節點的值和當前節點的值比較。
  3. 如果新節點的值更大,則以當前節點的右子節點作爲新的當前節點;如果新節點的值更小,則以當前節點的左子節點作爲新的當前節點。
  4. 重複 2、3 兩個步驟,直到搜索到合適的葉子節點爲止。
  5. 將新節點添加爲第 4 步找到的葉子節點的子節點;如果新節點更大,則添加爲右子節點;否則添加爲左子節點。

掌握上面理論之後,下面我們來分析 TreeMap 添加節點(TreeMap 中使用 Entry 內部類代表節點)的實現,TreeMap 集合的 put(K key, V value) 方法實現了將 Entry 放入排序二叉樹中,下面是該方法的源代碼:

 public V put(K key, V value) 
 { 
    // 先以 t 保存鏈表的 root 節點
    Entry<K,V> t = root; 
    // 如果 t==null,表明是一個空鏈表,即該 TreeMap 裏沒有任何 Entry 
    if (t == null) 
    { 
        // 將新的 key-value 創建一個 Entry,並將該 Entry 作爲 root 
        root = new Entry<K,V>(key, value, null); 
        // 設置該 Map 集合的 size 爲 1,代表包含一個 Entry 
        size = 1; 
        // 記錄修改次數爲 1 
        modCount++; 
        return null; 
    } 
    int cmp; 
    Entry<K,V> parent; 
    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; 
            // 如果兩個 key 相等,新的 value 覆蓋原有的 value,
            // 並返回原有的 value 
            else 
                return t.setValue(value); 
        } while (t != null); 
    } 
    else 
    { 
        if (key == null) 
            throw new NullPointerException(); 
        Comparable<? super K> k = (Comparable<? super K>) key; 
        do { 
            // 使用 parent 上次循環後的 t 所引用的 Entry 
            parent = t; 
            // 拿新插入 key 和 t 的 key 進行比較
            cmp = k.compareTo(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; 
            // 如果兩個 key 相等,新的 value 覆蓋原有的 value,
            // 並返回原有的 value 
            else 
                return t.setValue(value); 
        } while (t != null); 
    } 
    // 將新插入的節點作爲 parent 節點的子節點
    Entry<K,V> e = new Entry<K,V>(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; 
 }

上面程序中粗體字代碼就是實現“排序二叉樹”的關鍵算法,每當程序希望添加新節點時:系統總是從樹的根節點開始比較 —— 即將根節點當成當前節點,如果新增節點大於當前節點、並且當前節點的右子節點存在,則以右子節點作爲當前節點;如果新增節點小於當前節點、並且當前節點的左子節點存在,則以左子節點作爲當前節點;如果新增節點等於當前節點,則用新增節點覆蓋當前節點,並結束循環 —— 直到找到某個節點的左、右子節點不存在,將新節點添加該節點的子節點 —— 如果新節點比該節點大,則添加爲右子節點;如果新節點比該節點小,則添加爲左子節點。

TreeMap 的刪除節點

當程序從排序二叉樹中刪除一個節點之後,爲了讓它依然保持爲排序二叉樹,程序必須對該排序二叉樹進行維護。維護可分爲如下幾種情況:

(1)被刪除的節點是葉子節點,則只需將它從其父節點中刪除即可。

(2)被刪除節點 p 只有左子樹,將 p 的左子樹 pL 添加成 p 的父節點的左子樹即可;被刪除節點 p 只有右子樹,將 p 的右子樹 pR 添加成 p 的父節點的右子樹即可。

(3)若被刪除節點 p 的左、右子樹均非空,有兩種做法:

  • 將 pL 設爲 p 的父節點 q 的左或右子節點(取決於 p 是其父節點 q 的左、右子節點),將 pR 設爲 p 節點的中序前趨節點 s 的右子節點(s 是 pL 最右下的節點,也就是 pL 子樹中最大的節點)。
  • 以 p 節點的中序前趨或後繼替代 p 所指節點,然後再從原排序二叉樹中刪去中序前趨或後繼節點即可。(也就是用大於 p 的最小節點或小於 p 的最大節點代替 p 節點即可)。

圖 2 顯示了被刪除節點只有左子樹的示意圖:

圖 2. 被刪除節點只有左子樹
圖 2. 被刪除節點只有左子樹

圖 3 顯示了被刪除節點只有右子樹的示意圖:

圖 3. 被刪除節點只有右子樹
圖 3. 被刪除節點只有右子樹

圖 4 顯示了被刪除節點既有左子節點,又有右子節點的情形,此時我們採用到是第一種方式進行維護:

圖 4. 被刪除節點既有左子樹,又有右子樹
圖 4. 被刪除節點既有左子樹,又有右子樹

圖 5 顯示了被刪除節點既有左子樹,又有右子樹的情形,此時我們採用到是第二種方式進行維護:

圖 5. 被刪除節點既有左子樹,又有右子樹
圖 5. 被刪除節點既有左子樹,又有右子樹

TreeMap 刪除節點採用圖 5 所示右邊的情形進行維護——也就是用被刪除節點的右子樹中最小節點與被刪節點交換的方式進行維護。

TreeMap 刪除節點的方法由如下方法實現:

 private void deleteEntry(Entry<K,V> p) 
 { 
    modCount++; 
    size--; 
    // 如果被刪除節點的左子樹、右子樹都不爲空
    if (p.left != null && p.right != null) 
    { 
        // 用 p 節點的中序後繼節點代替 p 節點
        Entry<K,V> s = successor (p); 
        p.key = s.key; 
        p.value = s.value; 
        p = s; 
    } 
    // 如果 p 節點的左節點存在,replacement 代表左節點;否則代表右節點。
    Entry<K,V> replacement = (p.left != null ? p.left : p.right); 
    if (replacement != null) 
    { 
        replacement.parent = p.parent; 
        // 如果 p 沒有父節點,則 replacemment 變成父節點
        if (p.parent == null) 
            root = replacement; 
        // 如果 p 節點是其父節點的左子節點
        else if (p == p.parent.left) 
            p.parent.left  = replacement; 
        // 如果 p 節點是其父節點的右子節點
        else 
            p.parent.right = replacement; 
        p.left = p.right = p.parent = null; 
        // 修復紅黑樹
        if (p.color == BLACK) 
            fixAfterDeletion(replacement);       // ①
    } 
    // 如果 p 節點沒有父節點
    else if (p.parent == null) 
    { 
        root = null; 
    } 
    else 
    { 
        if (p.color == BLACK) 
            // 修復紅黑樹
            fixAfterDeletion(p);                 // ②
        if (p.parent != null) 
        { 
            // 如果 p 是其父節點的左子節點
            if (p == p.parent.left) 
                p.parent.left = null; 
            // 如果 p 是其父節點的右子節點
            else if (p == p.parent.right) 
                p.parent.right = null; 
            p.parent = null; 
        } 
    } 
 }

紅黑樹

排序二叉樹雖然可以快速檢索,但在最壞的情況下:如果插入的節點集本身就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後得到的排序二叉樹將變成鏈表:所有節點只有左節點(如果插入節點集本身是大到小排列);或所有節點只有右節點(如果插入節點集本身是小到大排列)。在這種情況下,排序二叉樹就變成了普通鏈表,其檢索效率就會很差。

爲了改變排序二叉樹存在的不足,Rudolf Bayer 與 1972 年發明了另一種改進後的排序二叉樹:紅黑樹,他將這種排序二叉樹稱爲“對稱二叉 B 樹”,而紅黑樹這個名字則由 Leo J. Guibas 和 Robert Sedgewick 於 1978 年首次提出。

紅黑樹是一個更高效的檢索二叉樹,因此常常用來實現關聯數組。典型地,JDK 提供的集合類 TreeMap 本身就是一個紅黑樹的實現。

紅黑樹在原有的排序二叉樹增加了如下幾個要求:

Java 實現的紅黑樹

上面的性質 3 中指定紅黑樹的每個葉子節點都是空節點,而且並葉子節點都是黑色。但 Java 實現的紅黑樹將使用 null 來代表空節點,因此遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每個葉子節點都是紅色的。

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

Java 中實現的紅黑樹可能有如圖 6 所示結構:

圖 6. Java 紅黑樹的示意
圖 6. Java 紅黑樹的示意

備註:本文中所有關於紅黑樹中的示意圖採用白色代表紅色。黑色節點還是採用了黑色表示。

根據性質 5:紅黑樹從根節點到每個葉子節點的路徑都包含相同數量的黑色節點,因此從根節點到葉子節點的路徑中包含的黑色節點數被稱爲樹的“黑色高度(black-height)”。

性質 4 則保證了從根節點到葉子節點的最長路徑的長度不會超過任何其他路徑的兩倍。假如有一棵黑色高度爲 3 的紅黑樹:從根節點到葉節點的最短路徑長度是 2,該路徑上全是黑色節點(黑節點 - 黑節點 - 黑節點)。最長路徑也只可能爲 4,在每個黑色節點之間插入一個紅色節點(黑節點 - 紅節點 - 黑節點 - 紅節點 - 黑節點),性質 4 保證絕不可能插入更多的紅色節點。由此可見,紅黑樹中最長路徑就是一條紅黑交替的路徑。

紅黑樹和平衡二叉樹

紅黑樹並不是真正的平衡二叉樹,但在實際應用中,紅黑樹的統計性能要高於平衡二叉樹,但極端性能略差。

由此我們可以得出結論:對於給定的黑色高度爲 N 的紅黑樹,從根到葉子節點的最短路徑長度爲 N-1,最長路徑長度爲 2 * (N-1)。

提示:排序二叉樹的深度直接影響了檢索的性能,正如前面指出,當插入節點本身就是由小到大排列時,排序二叉樹將變成一個鏈表,這種排序二叉樹的檢索性能最低:N 個節點的二叉樹深度就是 N-1。

紅黑樹通過上面這種限制來保證它大致是平衡的——因爲紅黑樹的高度不會無限增高,這樣保證紅黑樹在最壞情況下都是高效的,不會出現普通排序二叉樹的情況。

由於紅黑樹只是一個特殊的排序二叉樹,因此對紅黑樹上的只讀操作與普通排序二叉樹上的只讀操作完全相同,只是紅黑樹保持了大致平衡,因此檢索性能比排序二叉樹要好很多。

但在紅黑樹上進行插入操作和刪除操作會導致樹不再符合紅黑樹的特徵,因此插入操作和刪除操作都需要進行一定的維護,以保證插入節點、刪除節點後的樹依然是紅黑樹。

添加節點後的修復

上面 put(K key, V value) 方法中①號代碼處使用fixAfterInsertion(e) 方法來修復紅黑樹——因此每次插入節點後必須進行簡單修復,使該排序二叉樹滿足紅黑樹的要求。

插入操作按如下步驟進行:

(1)以排序二叉樹的方法插入新節點,並將它設爲紅色。

(2)進行顏色調換和樹旋轉。

插入後的修復

在插入操作中,紅黑樹的性質 1 和性質 3 兩個永遠不會發生改變,因此無需考慮紅黑樹的這兩個特性。

這種顏色調用和樹旋轉就比較複雜了,下面將分情況進行介紹。在介紹中,我們把新插入的節點定義爲 N 節點,N 節點的父節點定義爲 P 節點,P 節點的兄弟節點定義爲 U 節點,P 節點父節點定義爲 G 節點。

下面分成不同情形來分析插入操作

情形 1:新節點 N 是樹的根節點,沒有父節點

在這種情形下,直接將它設置爲黑色以滿足性質 2。

情形 2:新節點的父節點 P 是黑色

在這種情況下,新插入的節點是紅色的,因此依然滿足性質 4。而且因爲新節點 N 有兩個黑色葉子節點;但是由於新節點 N 是紅色,通過它的每個子節點的路徑依然保持相同的黑色節點數,因此依然滿足性質 5。

情形 3:如果父節點 P 和父節點的兄弟節點 U 都是紅色

在這種情況下,程序應該將 P 節點、U 節點都設置爲黑色,並將 P 節點的父節點設爲紅色(用來保持性質 5)。現在新節點 N 有了一個黑色的父節點 P。由於從 P 節點、U 節點到根節點的任何路徑都必須通過 G 節點,在這些路徑上的黑節點數目沒有改變(原來有葉子和 G 節點兩個黑色節點,現在有葉子和 P 兩個黑色節點)。

經過上面處理後,紅色的 G 節點的父節點也有可能是紅色的,這就違反了性質 4,因此還需要對 G 節點遞歸地進行整個過程(把 G 當成是新插入的節點進行處理即可)。

圖 7 顯示了這種處理過程:

圖 7. 插入節點後進行顏色調換
圖 7. 插入節點後進行顏色調換

備註:雖然圖 11.28 繪製的是新節點 N 作爲父節點 P 左子節點的情形,其實新節點 N 作爲父節點 P 右子節點的情況與圖 11.28 完全相同。

情形 4:父節點 P 是紅色、而其兄弟節點 U 是黑色或缺少;且新節點 N 是父節點 P 的右子節點,而父節點 P 又是其父節點 G 的左子節點。

在這種情形下,我們進行一次左旋轉對新節點和其父節點進行,接着按情形 5 處理以前的父節點 P(也就是把 P 當成新插入的節點即可)。這導致某些路徑通過它們以前不通過的新節點 N 或父節點 P 的其中之一,但是這兩個節點都是紅色的,因此不會影響性質 5。

圖 8 顯示了對情形 4 的處理:

圖 8. 插入節點後的樹旋轉
圖 8. 插入節點後的樹旋轉

備註:圖 11.29 中 P 節點是 G 節點的左子節點,如果 P 節點是其父節點 G 節點的右子節點,那麼上 面的處理情況應該左、右對調一下。

情形 5:父節點 P 是紅色、而其兄弟節點 U 是黑色或缺少;且新節點 N 是其父節點的左子節點,而父節點 P 又是其父節點 G 的左子節點。

在這種情形下,需要對節點 G 的一次右旋轉,在旋轉產生的樹中,以前的父節點 P 現在是新節點 N 和節點 G 的父節點。由於以前的節點 G 是黑色,否則父節點 P 就不可能是紅色,我們切換以前的父節點 P 和節點 G 的顏色,使之滿足性質 4,性質 5 也仍然保持滿足,因爲通過這三個節點中任何一個的所有路徑以前都通過節點 G,現在它們都通過以前的父節點 P。在各自的情形下,這都是三個節點中唯一的黑色節點。

圖 9 顯示了情形 5 的處理過程:

圖 9. 插入節點後的顏色調整、樹旋轉
圖 9. 插入節點後的顏色調整、樹旋轉

備註:圖 11.30 中 P 節點是 G 節點的左子節點,如果 P 節點是其父節點 G 節點的右子節點,那麼上面的處理情況應該左、右對調一下。

TreeMap 爲插入節點後的修復操作由 fixAfterInsertion(Entry<K,V> x) 方法提供,該方法的源代碼如下:

 // 插入節點後修復紅黑樹
 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 
            { 
                // 如果 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 的父節點的父節點設爲紅色
                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 在刪除之後的修復操作由 fixAfterDeletion(Entry<K,V> x) 方法提供,該方法源代碼如下:

 // 刪除節點後修復紅黑樹
 private void fixAfterDeletion(Entry<K,V> x) 
 { 
    // 直到 x 不是根節點,且 x 的顏色是黑色
    while (x != root && colorOf(x) == BLACK) 
    { 
        // 如果 x 是其父節點的左子節點
        if (x == leftOf(parentOf(x))) 
        { 
            // 獲取 x 節點的兄弟節點
            Entry<K,V> sib = rightOf(parentOf(x)); 
            // 如果 sib 節點是紅色
            if (colorOf(sib) == RED) 
            { 
                // 將 sib 節點設爲黑色
                setColor(sib, BLACK); 
                // 將 x 的父節點設爲紅色
                setColor(parentOf(x), RED); 
                rotateLeft(parentOf(x)); 
                // 再次將 sib 設爲 x 的父節點的右子節點
                sib = rightOf(parentOf(x)); 
            } 
            // 如果 sib 的兩個子節點都是黑色
            if (colorOf(leftOf(sib)) == BLACK 
                && colorOf(rightOf(sib)) == BLACK) 
            { 
                // 將 sib 設爲紅色
                setColor(sib, RED); 
                // 讓 x 等於 x 的父節點
                x = parentOf(x); 
            } 
            else 
            { 
                // 如果 sib 的只有右子節點是黑色
                if (colorOf(rightOf(sib)) == BLACK) 
                { 
                    // 將 sib 的左子節點也設爲黑色
                    setColor(leftOf(sib), BLACK); 
                    // 將 sib 設爲紅色
                    setColor(sib, RED); 
                    rotateRight(sib); 
                    sib = rightOf(parentOf(x)); 
                } 
                // 設置 sib 的顏色與 x 的父節點的顏色相同
                setColor(sib, colorOf(parentOf(x))); 
                // 將 x 的父節點設爲黑色
                setColor(parentOf(x), BLACK); 
                // 將 sib 的右子節點設爲黑色
                setColor(rightOf(sib), BLACK); 
                rotateLeft(parentOf(x)); 
                x = root; 
            } 
        } 
        // 如果 x 是其父節點的右子節點
        else 
        { 
            // 獲取 x 節點的兄弟節點
            Entry<K,V> sib = leftOf(parentOf(x)); 
            // 如果 sib 的顏色是紅色
            if (colorOf(sib) == RED) 
            { 
                // 將 sib 的顏色設爲黑色
                setColor(sib, BLACK); 
                // 將 sib 的父節點設爲紅色
                setColor(parentOf(x), RED); 
                rotateRight(parentOf(x)); 
                sib = leftOf(parentOf(x)); 
            } 
            // 如果 sib 的兩個子節點都是黑色
            if (colorOf(rightOf(sib)) == BLACK 
                && colorOf(leftOf(sib)) == BLACK) 
            { 
                // 將 sib 設爲紅色
                setColor(sib, RED); 
                // 讓 x 等於 x 的父節點
                x = parentOf(x); 
            } 
            else 
            { 
                // 如果 sib 只有左子節點是黑色
                if (colorOf(leftOf(sib)) == BLACK) 
                { 
                    // 將 sib 的右子節點也設爲黑色
                    setColor(rightOf(sib), BLACK); 
                    // 將 sib 設爲紅色
                    setColor(sib, RED); 
                    rotateLeft(sib); 
                    sib = leftOf(parentOf(x)); 
                } 
                // 將 sib 的顏色設爲與 x 的父節點顏色相同
                setColor(sib, colorOf(parentOf(x))); 
                // 將 x 的父節點設爲黑色
                setColor(parentOf(x), BLACK); 
                // 將 sib 的左子節點設爲黑色
                setColor(leftOf(sib), BLACK); 
                rotateRight(parentOf(x)); 
                x = root; 
            } 
        } 
    } 
    setColor(x, BLACK); 
 }

檢索節點

當 TreeMap 根據 key 來取出 value 時,TreeMap 對應的方法如下:

 public V get(Object key) 
 { 
    // 根據指定 key 取出對應的 Entry 
    Entry>K,V< p = getEntry(key); 
    // 返回該 Entry 所包含的 value 
    return (p==null ? null : p.value); 
 }

從上面程序的粗體字代碼可以看出,get(Object key) 方法實質是由於 getEntry() 方法實現的,這個 getEntry() 方法的代碼如下:

 final Entry<K,V> getEntry(Object key) 
 { 
    // 如果 comparator 不爲 null,表明程序採用定製排序
    if (comparator != null) 
        // 調用 getEntryUsingComparator 方法來取出對應的 key 
        return getEntryUsingComparator(key); 
    // 如果 key 形參的值爲 null,拋出 NullPointerException 異常
    if (key == null) 
        throw new NullPointerException(); 
    // 將 key 強制類型轉換爲 Comparable 實例
    Comparable<? super K> k = (Comparable<? super K>) key; 
    // 從樹的根節點開始
    Entry<K,V> p = root; 
    while (p != null) 
    { 
        // 拿 key 與當前節點的 key 進行比較
        int cmp = k.compareTo(p.key); 
        // 如果 key 小於當前節點的 key,向“左子樹”搜索
        if (cmp < 0) 
            p = p.left; 
        // 如果 key 大於當前節點的 key,向“右子樹”搜索
        else if (cmp > 0) 
            p = p.right; 
        // 不大於、不小於,就是找到了目標 Entry 
        else 
            return p; 
    } 
    return null; 
 }

上面的 getEntry(Object obj) 方法也是充分利用排序二叉樹的特徵來搜索目標 Entry,程序依然從二叉樹的根節點開始,如果被搜索節點大於當前節點,程序向“右子樹”搜索;如果被搜索節點小於當前節點,程序向“左子樹”搜索;如果相等,那就是找到了指定節點。

當 TreeMap 裏的 comparator != null 即表明該 TreeMap 採用了定製排序,在採用定製排序的方式下,TreeMap 採用 getEntryUsingComparator(key) 方法來根據 key 獲取 Entry。下面是該方法的代碼:

 final Entry<K,V> getEntryUsingComparator(Object key) 
 { 
    K k = (K) key; 
    // 獲取該 TreeMap 的 comparator 
    Comparator<? super K> cpr = comparator; 
    if (cpr != null) 
    { 
        // 從根節點開始
        Entry<K,V> p = root; 
        while (p != null) 
        { 
            // 拿 key 與當前節點的 key 進行比較
            int cmp = cpr.compare(k, p.key); 
            // 如果 key 小於當前節點的 key,向“左子樹”搜索
            if (cmp < 0) 
                p = p.left; 
            // 如果 key 大於當前節點的 key,向“右子樹”搜索
            else if (cmp > 0) 
                p = p.right; 
            // 不大於、不小於,就是找到了目標 Entry 
            else 
                return p; 
        } 
    } 
    return null; 
 }

其實 getEntry、getEntryUsingComparator 兩個方法的實現思路完全類似,只是前者對自然排序的 TreeMap 獲取有效,後者對定製排序的 TreeMap 有效。

通過上面源代碼的分析不難看出,TreeMap 這個工具類的實現其實很簡單。或者說:從內部結構來看,TreeMap 本質上就是一棵“紅黑樹”,而 TreeMap 的每個 Entry 就是該紅黑樹的一個節點。

轉載:http://www.ibm.com/developerworks/cn/java/j-lo-tree/#ibm-pcon

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章