1、簡介node
TreeMap最先出如今JDK 1.2中,是 Java 集合框架中比較重要一個的實現。TreeMap 底層基於紅黑樹實現,可保證在log(n)時間複雜度內完成 containsKey、get、put 和 remove 操做,效率很高。另外一方面,因爲 TreeMap 基於紅黑樹實現,這爲 TreeMap 保持鍵的有序性打下了基礎。總的來講,TreeMap 的核心是紅黑樹,其不少方法也是對紅黑樹增刪查基礎操做的一個包裝。因此只要弄懂了紅黑樹,TreeMap 就沒什麼祕密了。框架
「源碼分析
2、概覽學習
TreeMap繼承自AbstractMap,並實現了 NavigableMap接口。NavigableMap 接口繼承了SortedMap接口,SortedMap 最終繼承自Map接口,同時 AbstractMap 類也實現了 Map 接口。以上就是 TreeMap 的繼承體系,描述起來有點亂,不如看圖了:this
上圖就是 TreeMap 的繼承體系圖,比較直觀。這裏來簡單說一下繼承體系中不常見的接口NavigableMap和SortedMap,這兩個接口見名知意。先說 NavigableMap 接口,NavigableMap 接口聲明瞭一些列具備導航功能的方法,好比:orm
/**對象
* 返回紅黑樹中最小鍵所對應的 Entryblog
*/繼承
Map.EntryfirstEntry();接口
/**
* 返回最大的鍵 maxKey,且 maxKey 僅小於參數 key
*/
KlowerKey(K key);
/**
* 返回最小的鍵 minKey,且 minKey 僅大於參數 key
*/
KhigherKey(K key);
// 其餘略
經過這些導航方法,咱們能夠快速定位到目標的 key 或 Entry。至於 SortedMap 接口,這個接口提供了一些基於有序鍵的操做,好比
/**
* 返回包含鍵值在 [minKey, toKey) 範圍內的 Map
*/
SortedMapheadMap(K toKey);();
/**
* 返回包含鍵值在 [fromKey, toKey) 範圍內的 Map
*/
SortedMapsubMap(K fromKey, K toKey);
// 其餘略
以上就是兩個接口的介紹,很簡單。至於 AbstractMap 和 Map 這裏就不說了,你們有興趣本身去看看 Javadoc 吧。關於 TreeMap 的繼承體系就這裏就說到這,接下來咱們進入細節部分分析。
「
3、源碼分析
JDK 1.8中的TreeMap源碼有兩千多行,仍是比較多的。本文並不打算逐句分析全部的源碼,而是挑選幾個經常使用的方法進行分析。這些方法實現的功能分別是查找、遍歷、插入、刪除等,其餘的方法小夥伴們有興趣能夠本身分析。TreeMap實現的核心部分是關於紅黑樹的實現,其絕大部分的方法基本都是對底層紅黑樹增、刪、查操做的一個封裝。如簡介一節所說,只要弄懂了紅黑樹原理,TreeMap 就沒什麼祕密了。
TreeMap基於紅黑樹實現,而紅黑樹是一種自平衡二叉查找樹,因此 TreeMap 的查找操做流程和二叉查找樹一致。二叉樹的查找流程是這樣的,先將目標值和根節點的值進行比較,若是目標值小於根節點的值,則再和根節點的左孩子進行比較。若是目標值大於根節點的值,則繼續和根節點的右孩子比較。在查找過程當中,若是目標值和二叉樹中的某個節點值相等,則返回 true,不然返回 false。TreeMap 查找和此相似,只不過在 TreeMap 中,節點(Entry)存儲的是鍵值對。在查找過程當中,比較的是鍵的大小,返回的是值,若是沒找到,則返回null。TreeMap 中的查找方法是get,具體實如今getEntry方法中,相關源碼以下:
publicVget(Object key) {
Entry p = getEntry(key);
return(p==null?null: p.value);
}
finalEntry getEntry(Object key) {
// Offload comparator-based version for sake of performance
if(comparator !=null)
returngetEntryUsingComparator(key);
if(key ==null)
thrownew NullPointerException();
@SuppressWarnings("unchecked")
Comparable k = (Comparable) key;
Entry p = root;
// 查找操做的核心邏輯就在這個 while 循環裏
while(p !=null) {
int cmp = k.compareTo(p.key);
if(cmp <0)
p = p.left;
elseif(cmp >0)
p = p.right;
else
returnp;
}
returnnull;
}
查找操做的核心邏輯就是getEntry方法中的while循環,你們對照上面的說的流程,本身看一下吧,比較簡單,就很少說了。
3.2 遍歷
遍歷操做也是你們使用頻率較高的一個操做,對於TreeMap,使用方式通常以下:
for(Object key :map.keySet()) {
// do something
}
或
for(Map.Entry entry :map.entrySet()) {
// do something
}
從上面代碼片斷中能夠看出,你們通常都是對 TreeMap 的 key 集合或 Entry 集合進行遍歷。上面代碼片斷中用 foreach 遍歷keySet 方法產生的集合,在編譯時會轉換成用迭代器遍歷,等價於:
Setkeys = map.keySet();
Iterator ite = keys.iterator();
while(ite.hasNext()) {
Objectkey = ite.next();
// do something
}
另外一方面,TreeMap 有一個特性,便可以保證鍵的有序性,默認是正序。因此在遍歷過程當中,你們會發現 TreeMap 會從小到大輸出鍵的值。那麼,接下來就來分析一下keySet方法,以及在遍歷 keySet 方法產生的集合時,TreeMap 是如何保證鍵的有序性的。相關代碼以下:
publicSetkeySet(){
returnnavigableKeySet();
}
publicNavigableSetnavigableKeySet(){
KeySet nks = navigableKeySet;
return(nks !=null) ? nks : (navigableKeySet =newKeySet<>(this));
}
staticfinalclassKeySetextendsAbstractSetimplementsNavigableSet{
privatefinalNavigableMap m;
KeySet(NavigableMap map) { m = map; }
publicIteratoriterator(){
if(minstanceofTreeMap)
return((TreeMap)m).keyIterator();
else
return((TreeMap.NavigableSubMap)m).keyIterator();
}
// 省略非關鍵代碼
}
IteratorkeyIterator(){
returnnewKeyIterator(getFirstEntry());
}
finalclassKeyIteratorextendsPrivateEntryIterator{
KeyIterator(Entry first) {
super(first);
}
publicKnext(){
returnnextEntry().key;
}
}
abstractclassPrivateEntryIteratorimplementsIterator{
Entry next;
Entry lastReturned;
intexpectedModCount;
PrivateEntryIterator(Entry first) {
expectedModCount = modCount;
lastReturned =null;
next = first;
}
publicfinalbooleanhasNext(){
returnnext !=null;
}
finalEntrynextEntry(){
Entry e = next;
if(e ==null)
thrownewNoSuchElementException();
if(modCount != expectedModCount)
thrownewConcurrentModificationException();
// 尋找節點 e 的後繼節點
next = successor(e);
lastReturned = e;
returne;
}
// 其餘方法省略
}
上面的代碼比較多,keySet 涉及的代碼仍是比較多的,你們能夠從上往下看。從上面源碼能夠看出 keySet 方法返回的是KeySet類的對象。這個類實現了Iterable接口,能夠返回一個迭代器。該迭代器的具體實現是KeyIterator,而 KeyIterator 類的核心邏輯是在PrivateEntryIterator中實現的。上面的代碼雖多,但核心代碼仍是 KeySet 類和 PrivateEntryIterator 類的 nextEntry方法。KeySet 類就是一個集合,這裏不分析了。而 nextEntry 方法比較重要,下面簡單分析一下。
在初始化 KeyIterator 時,會將 TreeMap 中包含最小鍵的 Entry 傳給 PrivateEntryIterator。當調用 nextEntry 方法時,經過調用 successor 方法找到當前 entry 的後繼,並讓 next 指向後繼,最後返回當前的 entry。經過這種方式便可實現按正序返回鍵值的的邏輯。
好了,TreeMap 的遍歷操做就講到這。遍歷操做自己不難,但講的有點多,略顯囉嗦,你們見怪。
3.3 插入
相對於前兩個操做,插入操做明顯要複雜一些。當往 TreeMap 中放入新的鍵值對後,可能會破壞紅黑樹的性質。這裏爲了描述方便,把 Entry 稱爲節點。並把新插入的節點稱爲N,N 的父節點爲P。P 的父節點爲G,且 P 是 G 的左孩子。P 的兄弟節點爲U。在往紅黑樹中插入新的節點 N 後(新節點爲紅色),會產生下面5種狀況:
一、N 是根節點;
二、N 的父節點是黑色;
三、N 的父節點是紅色,叔叔節點也是紅色;
四、N 的父節點是紅色,叔叔節點是黑色,且 N 是 P 的右孩子;
五、N 的父節點是紅色,叔叔節點是黑色,且 N 是 P 的左孩子。
上面5中狀況中,狀況2不會破壞紅黑樹性質,因此無需處理。狀況1 會破壞紅黑樹性質2(根是黑色),狀況三、四、和5會破壞紅黑樹性質4(每一個紅色節點必須有兩個黑色的子節點)。這個時候就須要進行調整,以使紅黑樹從新恢復平衡。接下來分析一下插入操做相關源碼:
publicVput(K key, Vvalue){
Entry t = root;
// 1.若是根節點爲 null,將新節點設爲根節點
if(t ==null) {
compare(key, key);
root =newEntry<>(key,value,null);
size =1;
modCount++;
returnnull;
}
intcmp;
Entry parent;
// split comparator and comparable paths
Comparator cpr = comparator;
if(cpr !=null) {
// 2.爲 key 在紅黑樹找到合適的位置
do{
parent = t;
cmp = cpr.compare(key, t.key);
if(cmp <0)
t = t.left;
elseif(cmp >0)
t = t.right;
else
returnt.setValue(value);
}while(t !=null);
}else{
// 與上面代碼邏輯相似,省略
}
Entry e =newEntry<>(key,value, parent);
// 3.將新節點鏈入紅黑樹中
if(cmp <0)
parent.left = e;
else
parent.right = e;
// 4.插入新節點可能會破壞紅黑樹性質,這裏修正一下
fixAfterInsertion(e);
size++;
modCount++;
returnnull;
}
put 方法代碼如上,邏輯和二叉查找樹插入節點邏輯一致。重要的步驟我已經寫了註釋,並不難理解。插入邏輯的複雜之處在於插入後的修復操做,對應的方法fixAfterInsertion,該方法的源碼和說明以下:
到這裏,插入操做就講完了。接下來,來講說 TreeMap 中最複雜的部分,也就是刪除操做了。
3.4 刪除
刪除操做是紅黑樹最複雜的部分,緣由是該操做可能會破壞紅黑樹性質5(從任一節點到其每一個葉子的全部簡單路徑都包含相同數目的黑色節點),修復性質5要比修復其餘性質(性質2和4需修復,性質1和3不用修復)複雜的多。當刪除操做致使性質5被破壞時,會出現8種狀況。爲了方便表述,這裏仍是先作一些假設。咱們把最終被刪除的節點稱爲 X,X 的替換節點稱爲 N。N 的父節點爲P,且 N 是 P 的左孩子。N 的兄弟節點爲S,S 的左孩子爲 SL,右孩子爲 SR。這裏特意強調 X 是 最終被刪除 的節點,是緣由二叉查找樹會把要刪除有兩個孩子的節點的狀況轉化爲刪除只有一個孩子的節點的狀況,該節點是欲被刪除節點的前驅和後繼。
接下來,簡單列舉一下刪除節點時可能會出現的狀況,先列舉較爲簡單的狀況:
一、最終被刪除的節點 X 是紅色節點;
二、X 是黑色節點,但該節點的孩子節點是紅色。
比較複雜的狀況:
一、替換節點 N 是新的根;
二、N 爲黑色,N 的兄弟節點 S 爲紅色,其餘節點爲黑色;
三、N 爲黑色,N 的父節點 P,兄弟節點 S 和 S 的孩子節點均爲黑色;
四、N 爲黑色,P 是紅色,S 和 S 孩子均爲黑色;
五、N 爲黑色,P 可紅可黑,S 爲黑色,S 的左孩子 SL 爲紅色,右孩子 SR 爲黑色;
六、N 爲黑色,P 可紅可黑,S 爲黑色,SR 爲紅色,SL 可紅可黑。
上面列舉的8種狀況中,前兩種處理起來比較簡單,後6種狀況中狀況26較爲複雜。接下來我將會對狀況26展開分析,刪除相關的源碼以下:
publicV remove(Object key) {
Entry p = getEntry(key);
if(p ==null)
returnnull;
V oldValue = p.value;
deleteEntry(p);
returnoldValue;
}
privatevoid deleteEntry(Entry p) {
modCount++;
size--;
/*
* 1. 若是 p 有兩個孩子節點,則找到後繼節點,
* 並把後繼節點的值複製到節點 P 中,並讓 p 指向其後繼節點
*/
if(p.left !=null&& p.right !=null) {
Entry s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}// p has 2 children
// Start fixup at replacement node, if it exists.
Entry replacement = (p.left !=null? p.left : p.right);
if(replacement !=null) {
/*
* 2. 將 replacement parent 引用指向新的父節點,
* 同時讓新的父節點指向 replacement。
*/
replacement.parent= p.parent;
if(p.parent==null)
root = replacement;
elseif(p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent=null;
// 3. 若是刪除的節點 p 是黑色節點,則須要進行調整
if(p.color == BLACK)
fixAfterDeletion(replacement);
}elseif(p.parent==null) {// 刪除的是根節點,且樹中當前只有一個節點
root =null;
}else{// 刪除的節點沒有孩子節點
// p 是黑色,則須要進行調整
if(p.color == BLACK)
fixAfterDeletion(p);
// 將 P 從樹中移除
if(p.parent!=null) {
if(p == p.parent.left)
p.parent.left =null;
elseif(p == p.parent.right)
p.parent.right =null;
p.parent=null;
}
}
}
從源碼中能夠看出,remove方法只是一個簡單的保證,核心實如今deleteEntry方法中。deleteEntry 主要作了這麼幾件事:
一、若是待刪除節點 P 有兩個孩子,則先找到 P 的後繼 S,而後將 S 中的值拷貝到 P 中,並讓 P 指向 S;
二、若是最終被刪除節點 P(P 如今指向最終被刪除節點)的孩子不爲空,則用其孩子節點替換掉;
三、若是最終被刪除的節點是黑色的話,調用 fixAfterDeletion 方法進行修復。
上面說了 replacement 不爲空時,deleteEntry 的執行邏輯。上面說的略微囉嗦,若是簡單說的話,7個字便可總結:找後繼 -> 替換 -> 修復。這三步中,最複雜的是修復操做。修復操做要從新使紅黑樹恢復平衡,修復操做的源碼分析以下:
fixAfterDeletion 方法分析以下:
上面對 fixAfterDeletion 部分代碼邏輯就好了分析,經過配圖的形式解析了每段代碼邏輯所處理的狀況。經過圖解,應該仍是比較好理解的。好了,TreeMap 源碼先分析到這裏。
「
4、總結
本文則從實踐層面是分析了插入和刪除操做在具體的實現中時怎樣作的。另外,本文選擇了從集合框架經常使用方法這一角度進行分析,詳細分析了查找、遍歷、插入和刪除等方法。整體來講,分析的仍是比較詳細的。固然限於本人的水平,文中可能會存在一些錯誤的論述。若是你們發現了,歡迎指出來。若是這些錯誤的論述對你形成了困擾,我這裏先說聲抱歉。若是你也在學習 TreeMap 源碼,但願這篇文章可以幫到你。
最後感謝你們花時間的閱讀個人文章,順祝你們寫代碼無BUG,下篇文章見。
若是您以爲不錯,請別忘了轉發、分享、點贊讓更多的人去學習