Java TreeMap 源碼解析

這篇文章開始介紹Map系列另外一個比較重要的類TreeMap。 你們也許能感受到,網絡上介紹HashMap的文章比較多,可是介紹TreeMap反而不那麼多,這裏面是有緣由:一方面HashMap的使用場景比較多;二是相對於HashMap來講,TreeMap所用到的數據結構更爲複雜。 廢話很少說,進入正題。java

簽名(signature)

能夠看到,相比HashMap來講,TreeMap多繼承了一個接口NavigableMap,也就是這個接口,決定了TreeMap與HashMap的不一樣:算法

HashMap的key是無序的,TreeMap的key是有序的網絡

接口NavigableMap

首先看下NavigableMap的簽名數據結構

1
public interface NavigableMap<K,V> extends SortedMap<K,V>

發現NavigableMap繼承了SortedMap,再看SortedMap的簽名優化

SortedMap

1
public interface SortedMap<K,V> extends Map<K,V>

SortedMap就像其名字那樣,說明這個Map是有序的。這個順序通常是指由Comparable接口提供的keys的天然序(natural ordering),或者也能夠在建立SortedMap實例時,指定一個Comparator來決定。 當咱們在用集合視角(collection views,與HashMap同樣,也是由entrySet、keySet與values方法提供)來迭代(iterate)一個SortedMap實例時會體現出key的順序。 這裏引伸下關於Comparable與Comparator的區別(參考這裏):spa

  • Comparable通常表示類的天然序,好比定義一個Student類,學號爲默認排序設計

  • Comparator通常表示類在某種場合下的特殊分類,須要定製化排序。好比如今想按照Student類的age來排序code

插入SortedMap中的key的類類都必須繼承Comparable類(或指定一個comparator),這樣才能肯定如何比較(經過k1.compareTo(k2)comparator.compare(k1, k2))兩個key,不然,在插入時,會報ClassCastException的異常。 此爲,SortedMap中key的順序性應該與equals方法保持一致。也就是說k1.compareTo(k2)comparator.compare(k1, k2)爲true時,k1.equals(k2)也應該爲true。 介紹完了SortedMap,再來回到咱們的NavigableMap上面來。 NavigableMap是JDK1.6新增的,在SortedMap的基礎上,增長了一些「導航方法」(navigation methods)來返回與搜索目標最近的元素。例以下面這些方法:orm

  • lowerEntry,返回全部比給定Map.Entry小的元素視頻

  • floorEntry,返回全部比給定Map.Entry小或相等的元素

  • ceilingEntry,返回全部比給定Map.Entry大或相等的元素

  • higherEntry,返回全部比給定Map.Entry大的元素

設計理念(design concept)

紅黑樹(Red–black tree)

TreeMap是用紅黑樹做爲基礎實現的,紅黑樹是一種二叉搜索樹,讓咱們在一塊兒回憶下二叉搜索樹的一些性質

二叉搜索樹

先看看二叉搜索樹(binary search tree,BST)長什麼樣呢?

 

二叉搜索樹

相信你們對這個圖都不陌生,關鍵點是:

 

左子樹的值小於根節點,右子樹的值大於根節點。

二叉搜索樹的優點在於每進行一次判斷就是能將問題的規模減小一半,因此若是二叉搜索樹是平衡的話,查找元素的時間複雜度爲log(n),也就是樹的高度。 我這裏想到一個比較嚴肅的問題,若是說二叉搜索樹將問題規模減小了一半,那麼三叉搜索樹不就將問題規模減小了三分之二,這不是更好嘛,以此類推,咱們還能夠有四叉搜索樹,五叉搜索樹……對於更通常的狀況:

n個元素,K叉樹搜索樹的K爲多少時效率是最好的?K=2時嗎?

K 叉搜索樹

若是你們按照我上面分析,極可能也陷入一個誤區,就是

三叉搜索樹在將問題規模減小三分之二時,所需比較操做的次數是兩次(二叉搜索樹再將問題規模減小一半時,只須要一次比較操做)

咱們不能把這兩次給忽略了,對於更通常的狀況:

n個元素,K叉樹搜索樹須要的平均比較次數爲k*log(n/k)

對於極端狀況k=n時,K叉樹就轉化爲了線性表了,複雜度也就是O(n)了,若是用數學角度來解這個問題,至關於:

n爲固定值時,k取何值時,k*log(n/k)的取值最小?

k*log(n/k)根據對數的運算規則能夠轉化爲ln(n)*k/ln(k)ln(n)爲常數,因此至關於取k/ln(k)的極小值。這個問題對於大一剛學高數的人來講再簡單不過了,咱們這裏直接看結果

當k=e時,k/ln(k)取最小值。

天然數e的取值大約爲2.718左右,能夠看到二叉樹基本上就是這樣最優解了。在Nodejs的REPL中進行下面的操做

貌似k=3時比k=2時獲得的結果還要小,那也就是說三叉搜索樹應該比二叉搜索樹更好些呀,可是爲何二叉樹更流行呢?後來在萬能的stackoverflow上找到了答案,主旨以下:

如今的CPU能夠針對二重邏輯(binary logic)的代碼作優化,三重邏輯會被分解爲多個二重邏輯。

這樣也就大概能理解爲何二叉樹這麼流行了,就是由於進行一次比較操做,咱們最多能夠將問題規模減小一半。 好了這裏扯的有點遠了,咱們再回到紅黑樹上來。

紅黑樹性質

先看看紅黑樹的樣子:

 

紅黑樹示例

上圖是從wiki截來的,須要說明的一點是:

 

葉子節點爲上圖中的NIL節點,國內一些教材中沒有這個NIL節點,咱們在畫圖時有時也會省略這些NIL節點,可是咱們須要明確,當咱們說葉子節點時,指的就是這些NIL節點。

紅黑樹經過下面5條規則,保證了樹是平衡的:

  1. 樹的節點只有紅與黑兩種顏色

  2. 根節點爲黑色的

  3. 葉子節點爲黑色的

  4. 紅色節點的字節點一定是黑色的

  5. 從任意一節點出發,到其後繼的葉子節點的路徑中,黑色節點的數目相同

知足了上面5個條件後,就可以保證:根節點到葉子節點的最長路徑不會大於根節點到葉子最短路徑的2倍。 其實這個很好理解,主要是用了性質4與5,這裏簡單說下:

假設根節點到葉子節點最短的路徑中,黑色節點數目爲B,那麼根據性質5,根節點到葉子節點的最長路徑中,黑色節點數目也是B,最長的狀況就是每兩個黑色節點中間有個紅色節點(也就是紅黑相間的狀況),因此紅色節點最多爲B-1個。這樣就能證實上面的結論了。

紅黑樹操做

 

紅黑樹旋轉示例(沒有畫出NIL節點)

關於紅黑樹的插入、刪除、左旋、右旋這些操做,我以爲最好能夠作到可視化,文字表達比較繁瑣,我這裏就不在獻醜了,網上能找到的也比較多,像v_July_v的《教你透徹瞭解紅黑樹》。我這裏推薦個swf教學視頻(視頻爲英文,你們不要懼怕,重點是看圖??),7分鐘左右,你們能夠參考。 這裏還有個交互式紅黑樹的可視化網頁,你們能夠上去本身操做操做,插入幾個節點,刪除幾個節點玩玩,看看左旋右旋是怎麼玩的。

 

源碼剖析

因爲紅黑樹的操做我這裏不說了,因此這裏基本上也就沒什麼源碼能夠講了,由於這裏面重要的算法都是From CLR,這裏的CLR是指Cormen, Leiserson, Rivest,他們是算法導論的做者,也就是說TreeMap裏面算法都是參照算法導論的僞代碼。 由於紅黑樹是平衡的二叉搜索樹,因此其put(包含update操做)、get、remove的時間複雜度都爲log(n)

總結

到目前爲止,TreeMap與HashMap的的實現算是都介紹完了,能夠看到它們實現的不一樣,決定了它們應用場景的不一樣:

  • TreeMap的key是有序的,增刪改查操做的時間複雜度爲O(log(n)),爲了保證紅黑樹平衡,在必要時會進行旋轉

  • HashMap的key是無序的,增刪改查操做的時間複雜度爲O(1),爲了作到動態擴容,在必要時會進行resize。

另外,我這裏沒有解釋具體代碼,不免有些標題黨了,請你們見諒,後面理解的更深入了再來填坑。

相關文章
相關標籤/搜索