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。ui
與 HashSet 徹底相似的是,TreeSet 裏絕大部分方法都是直接調用 TreeMap 的方法來實現的,這一點讀者能夠自行參閱 TreeSet 的源代碼,此處就再也不給出了。this
對於 TreeMap 而言,它採用一種被稱爲「紅黑樹」的排序二叉樹來保存 Map 中每一個 Entry —— 每一個 Entry 都被當成「紅黑樹」的一個節點對待。例如對於以下程序而言:spa
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 所示二叉樹,中序遍歷得:
{2,3,4,8,9,9,10,13,15,18}
建立排序二叉樹的步驟,也就是不斷地向排序二叉樹添加節點的過程,向排序二叉樹添加節點的步驟以下:
- 以根節點當前節點開始搜索。
- 拿新節點的值和當前節點的值比較。
- 若是新節點的值更大,則以當前節點的右子節點做爲新的當前節點;若是新節點的值更小,則以當前節點的左子節點做爲新的當前節點。
- 重複 二、3 兩個步驟,直到搜索到合適的葉子節點爲止。
- 將新節點添加爲第 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. 被刪除節點只有左子樹

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

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

圖 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 紅黑樹的示意

備註:本文中全部關於紅黑樹中的示意圖採用白色表明紅色。黑色節點仍是採用了黑色表示。
根據性質 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. 插入節點後進行顏色調換

備註:雖然圖 11.28 繪製的是新節點 N 做爲父節點 P 左子節點的情形,其實新節點 N 做爲父節點 P 右子節點的狀況與圖 11.28 徹底相同。
情形 4:父節點 P 是紅色、而其兄弟節點 U 是黑色或缺乏;且新節點 N 是父節點 P 的右子節點,而父節點 P 又是其父節點 G 的左子節點。
在這種情形下,咱們進行一次左旋轉對新節點和其父節點進行,接着按情形 5 處理之前的父節點 P(也就是把 P 當成新插入的節點便可)。這致使某些路徑經過它們之前不經過的新節點 N 或父節點 P 的其中之一,可是這兩個節點都是紅色的,所以不會影響性質 5。
圖 8 顯示了對情形 4 的處理:
圖 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. 插入節點後的顏色調整、樹旋轉

備註:圖 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 就是該紅黑樹的一個節點。