Java之集合(十二)TreeMap


  轉載請注明源出處:http://www.cnblogs.com/lighten/p/7411935.html

1.前言

  本章介紹Map體系中的TreeMap,顧名思義,這個是一個樹結構的Map。TreeMap是一個具有比較器的Map,其是由比較器來決定get和put操作的,沒有比較器的時候就使用compareTo方法進行比較。其實現是我在HashMap中所講到的紅黑樹,但是實現方法卻與HashMap的實現有些許區別,所以這里再進行描述一次,如果hashMap那章沒看明白,可以再了解一下。

  TreeMap對於put,remove和get方法,保證其時間開銷是log(n)。這個類也是一個線程非安全的類。迭代器也同樣是fail-fast類型。

2.NavigableMap

  NavigableMap是一個接口,其繼承自SortedMap。

    

  上面是這兩個接口的所有方法定義。下面簡單介紹一下這些方法的含義。

    SortedMap(最后三個方法不描述):

  (1)comparator():返回sortedMap的比較器,如果為null,意味着使用默認的Comparable順序

  (2)subMap(K,K):返回map的一部分,范圍是[K,K)。key的大小范圍。

  (3)headMap(K):返回比所給key小的map部分

  (4)tailMap(K):返回比所給key大或等於的map部分

  (5)firstKey():返回當前map中最小的key

  (6)lastKey():返回當前map中最大的key

    NavigableMap:

  (1)lowerEntry(K):返回比所給key小且是其中最大的一個鍵值對

  (2)lowerKey(K):返回比所給key小且是其中最大的一個鍵

  (3)floorEntry(K):返回小於或等於所給key,且是其中最大的一個鍵值對

  (4)floorKey(K):返回小於或等於所給key,且是其中最大的一個鍵

  (5)ceilingEntry(K):返回大於或等於所給key,且是其中最小的一個鍵值對

  (6)ceilingKey(K):返回大於或等於所給key,且是其中最小的一個鍵

  (7)higherEntry(K):返回比所給key大且是其中最小的一個鍵值對

  (8)higherKey(K):返回比所給key大且是其中最小的一個鍵

  (9)firstEntry():返回當前map中key最小的鍵值對

  (10)lastEntry():返回當前map中key最大的鍵值對

  (11)pollFirstEntry():返回並移除當前map中key最小的鍵值對

  (12)pollLastEntry():返回並移除當前map中key最大的鍵值對

    后面的基本看名稱也知道其作用,不再進行介紹。

3.TreeMap

  TreeMap的結構是紅黑樹,十分的簡單,就一個根節點和比較器。剩下的都是通用的size和modCount屬性。下面介紹了一下get、put和remove這三個最基本的方法實現,其余的就略講。

  get方法十分清楚,有比較器的時候使用比較器的相關方法(實際和非比較器的沒什么區別,就比較那里使用了比較器比較而已)。從根節點開始,進行比較,小於的走左邊,大於的走右邊,等於的直接返回。這里就能看出,TreeMap與hashMap不同,其把比較結果相同的看做是同一個鍵,這個地方要小心了。樹的結構也很清楚了:左節點<父節點<右節點。下面看剛剛說的“坑處”:

    @Test
	public void testTreeMap() {
		TreeMap<Integer, String> tree = new TreeMap<>(new Comparator<Integer>() {
			@Override
			public int compare(Integer o1, Integer o2) {
				return 0;
			}
		});
		tree.put(1, "1");
		tree.put(2, "2");
		System.out.println("size:"+tree.size()+",1:"+tree.get(1));
	}

  上面輸出來的就是:size:1,1:2。1和2被看成了同一個鍵,使用TreeMap的時候這點要注意,否則會出現不想要的結果。

 

  put方法實現過長,這里就不進行截圖。實現也比較簡單,簡述一下具體過程:1.根節點為null,檢查放入根節點。2.比較器不為null,使用比較器循環比較,找到插入的父節點,記錄是放在左邊還是右邊,如果相等就更新值,直接返回。3.如果沒返回,意味着是新值,根據應插入的父節點和比較結果,放入具體的左邊還是右邊。4.這個是關鍵的步驟,為了保持紅黑樹的平衡,需要調整樹。5.修改size和modCount。

  上面的關鍵就是第四步,保持樹的平衡。此方法實現和HashMap的實現相比有小區別,但是思路看過去更清晰。

  每一個插入的結點都被看做是紅鏈接,調整完成之后根節點必須是非紅鏈接。這里還是給出HashMap中所給的紅黑樹基礎概念的參考文章:這里。關鍵是2-3樹的理解,就能更好的理解這段代碼了。每一個插入節點都被看成是紅鏈接,如果其父節點也是紅鏈接,就認為該節點可能需要進行調整。為什么這樣需要進行調整呢?看2-3樹插入分裂的過程:

  1.當前插入結點是2節點,將其變成一個3節點,3節點就是包含2個元素的結點;

  2.當前插入結點是3節點,將其變成一個4節點,4節點是包含3個元素的結點,這樣就可以很容易分裂成左-父-右節點;

    分裂之后會產生父節點歸屬的問題,所以會出現3種情況:

    (1)插入的3節點沒有父節點,則分裂出來的父節點為根節點

    (2)插入的3節點父節點是2節點,則分裂出來的父節點並入其中變成3節點

    (3)插入的3節點父節點是3節點,則分裂出來的父節點變成4節點,循環第2步再往上進行分裂

  2-3樹這種分裂方式會自動平衡選擇出根節點。紅黑樹將所有節點都看做是2節點,並且先當成是一個紅鏈接。紅鏈接的含義就是這是一個左鏈接(暫不清楚為什么是必須是左鏈接),並且與其父節點構成了3節點。所以根據2-3樹的定義,紅鏈接是不可能出現連續出現2個的,當前結點與父節點是一個紅鏈接,父節點與父父節點是一個紅鏈接,這樣就構成了一個包含3個元素的4節點,四節點根據上面所說就是需要分裂的。以上就是對判斷調整條件的說明。根節點一定不是紅鏈接,其沒有父節點進行合並。

  由於紅黑樹中每個插入節點是被看成是插入左邊並且與父節點構成了3節點,實際情況可能不是這樣。這就是第一個if判斷的情況了:1.其父節點真的是父父節點的左邊;2.其父節點不是父父節點的左邊。

  先看第一種情況

  第一種情況不確定的因素還是有很多:X是否是真的紅鏈接,即在XP的左邊,另外就是XPP右邊的情況。所以if塊里面的代碼就是為了明確這些情況來進行判斷是否需要進行相應操作的代碼。

  1.如果Y也是被認作是一個紅鏈接,此時Y當然不是一個左邊的結點,但是其含義是Y和XPP是在同一個節點中,這樣XP、XPP、Y就構成了一個4節點,X插入4節點中觸發了4節點的分裂(這個與2-3樹不同,2-3樹是3節點變成4節點就進行分裂了),用圖表示該段代碼的結果如下:

  4節點分裂,XPP歸到上面節點合並成一個節點,然后繼續循環處理XPP上面的相關內容了。由於每一步插入都保持平衡,此處XPP深度歸於上面節點,則沒有產生新的深度,此輪依舊平衡。

  2.如果Y不是一個紅鏈接的時候,此時Y也可能就不存在,此時X的插入就比較麻煩了。X很有可能是打破了樹的平衡,需要通過旋轉重新將樹保持平衡。旋轉的作用就是減少旋轉邊樹的深度,右旋就是減小左深度,左旋就是減小右深度,這句應該很好理解。理解不了看下圖,下圖給了一個左邊深度失衡,進行右旋的例子。

  X的插入雖然可能打破了平衡,但是並不清楚這個循環的X是否是葉子節點,葉子節點當然無所謂,但是非葉子節點的時候就需要進行判斷X是插入XP的左邊還是右邊。

  首先要明確,所有的操作都保證了之前樹是平衡的,現在插入X:

  ①如果在XP的右邊,其XP的平衡可能被打破,XP的右邊可能失衡,需要先進行左旋,維持XP的平衡,XP平衡后並不代表XPP平衡了,XPP的左邊可能失衡了,需要進行右旋保持XPP的左右平衡。

  ②如果在XP的左邊,由於之前就是平衡的,現在插入左邊失衡了,不需要管XP的左邊是否失衡,只要讓XPP保持平衡就可以了,所以進行XPP的右旋。

  第二種情況:X的父節點不在父父節點的左邊

  做法是相同的,先判斷左邊是不是構成了4節點需要分裂,左邊沒構成,再看X是在XP的左邊還是右邊,左邊的話需要保持XP的平衡,需要右旋,之后再保持XPP的平衡進行左旋。X在XP右邊就沒這么麻煩,只需要保持XPP的平衡,進行左旋就可以了。

  關於左旋右旋的方法,在HashMap那章中感覺已經介紹的很多了,這里不再進行介紹。鏈接點這里

   

  紅黑樹的刪除暫時沒有想清楚-。-???,等我研究明白再補充。

  雖然刪除操作還是有些沒看明白,但是還是記錄一些明白了的地方,如果之后想明白了,再繼續補充更新。下面是JDK8中的remove源碼:

  JDK將一個節點移除,會將其后繼節點的鍵值放入該節點。注意是鍵值改變了,對象並沒有變化。后繼節點也不是隨便挑選的,其通過方法successor(p),來找到后繼節點。后繼節點的選擇原則是盡可能選擇大於刪除鍵的最小值,過程是如果刪除節點存在右節點,選擇右節點的最左邊的子節點。如果沒有右節點,如果該節點是父節點的右邊,繼續判斷父節點是否是父父節點的右邊,最后當父節點左邊的時候,選擇那個父節點(父節點右邊,意味着刪除節點大於父節點,最后處於某一個子樹的左邊,意味着該子樹的父節點大於左邊的所有節點,這樣也滿足了大於刪除鍵的最小值)。代碼如下:

  這樣選出來的后繼節點都可以不破壞紅黑樹左節點<父節點<右節點的這一原則。剩下的就是處理這個后繼節點了(因為其值已經賦給移除節點,該后繼節點要從樹結構中移除)。移除后繼節點又分成了兩種可能。這就是if-elseif-else中的內容了:后繼節點分為不是葉子節點,或是葉子節點(又分為是根節點和非根節點)。根節點移除就十分簡單了,直接root=null就行了。如果非葉子節點,就需要將其子節點替換掉該節點,優先左節點,替換完了之后可能打破了樹的平衡,需要進行調整(這里有個問題沒想明白,如果后繼左右節點都存在,那后繼節點的右節點怎么辦,難道能保證后繼節點只存在一個子節點-。-?)。葉子節點只需要解除鏈接就可以了,但是同樣可能打破平衡,由於其是最后一個節點,所以必須先調整,再解除鏈接。打破平衡只可能發生在后繼節點為黑的時候,紅的話就沒必要了,移除依舊是平衡的,因為紅的高度實質上是隱藏了。

  fixAfterDeletion方法就是用來調整刪除后的結點順序,入參是后繼節點位置的那個節點。所以非葉子節點時,先將后繼節點的子節點上移了,移除占住了后繼節點,而在葉子節點時,沒有子節點,就不能先移除這個節點,必須先調整了。fixAfterDeletion只看一部分,另一部分是對稱的不進行敘述。

  循環條件當然是不為根節點,並且是可能移除打破平衡的黑節點。要跳出循環我們也可以看到,直接將x置為root節點就跳出了。這一段代碼還是有些疑惑,還是給出自己的理解。先看后繼節點X是其父節點左邊的情況:

  X是左節點,同級的右節點是紅節點,那么肯定打破了平衡,將XP這個子樹左旋,維護深度。不論第一步有沒有,XP的左右節點都是黑的時候,XP肯定是紅,不能通過調整XP子樹維持平衡,繼續以XP為X進入下一個循環,擴大調整范圍。否則第一步之后XP,通過XP的子樹調節又是可以調整好的,關注XP的右節點SIB,SIB的右節點是黑的,將其並入SIB進行右旋,先調整好SIB的高度。如果是紅的就不需要了,直接左旋。這里也只是有點感覺,為的就是只通過調整X的子樹來維護平衡,維護不了就擴大子樹進行,維護的了的時候會進行更下層的調整,讓下層調整能夠成功。大概是一個這個意思。

4.后記

  TreeMap中又重看了一遍紅黑樹,比HashMap中所說的要清楚一些,刪除操作還沒想明白,需要一段時間來解決。


免責聲明!

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



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