treemap 的實現就是紅黑樹數據結構,也就說是一棵自平衡的排序二叉樹,這樣就可以保證當需要快速檢索指定節點。
treeset 和 treemap 的關系
為了讓大家了解 treemap 和 treeset 之間的關系,下面先看 treeset 類的部分源代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
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 都被當成“紅黑樹”的一個節點對待。例如對于如下程序而言:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 由小到大地排列):
1
|
{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}
創建排序二叉樹的步驟,也就是不斷地向排序二叉樹添加節點的過程,向排序二叉樹添加節點的步驟如下:
- 以根節點當前節點開始搜索。
- 拿新節點的值和當前節點的值比較。
- 如果新節點的值更大,則以當前節點的右子節點作為新的當前節點;如果新節點的值更小,則以當前節點的左子節點作為新的當前節點。
- 重復 2、3 兩個步驟,直到搜索到合適的葉子節點為止。
- 將新節點添加為第 4 步找到的葉子節點的子節點;如果新節點更大,則添加為右子節點;否則添加為左子節點。
掌握上面理論之后,下面我們來分析 treemap 添加節點(treemap 中使用 entry 內部類代表節點)的實現,treemap 集合的 put(k key, v value) 方法實現了將 entry 放入排序二叉樹中,下面是該方法的源代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
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 刪除節點的方法由如下方法實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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) 方法提供,該方法的源代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
// 插入節點后修復紅黑樹 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) 方法提供,該方法源代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
// 刪除節點后修復紅黑樹 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 對應的方法如下:
1
2
3
4
5
6
7
|
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() 方法的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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。下面是該方法的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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.cnblogs.com/liqizhou/archive/2012/09/27/java%E4%B8%ADtreemap%E5%92%8Ctreeset%E5%AE%9E%E7%8E%B0%E7%BA%A2%E9%BB%91%E6%A0%91.html