Java集合框架原始碼剖析:TreeSet 和 TreeMap

CarpenterLee發表於2016-06-11

總體介紹

之所以把TreeSetTreeMap放在一起講解,是因為二者在Java裡有著相同的實現,前者僅僅是對後者做了一層包裝,也就是說TreeSet裡面有一個TreeMap(介面卡模式)。因此本文將重點分析TreeMap

Java TreeMap實現了SortedMap介面,也就是說會按照key的大小順序對Map中的元素進行排序,key大小的評判可以通過其本身的自然順序(natural ordering),也可以通過構造時傳入的比較器(Comparator)。

TreeMap底層通過紅黑樹(Red-Black tree)實現,也就意味著containsKey(), get(), put(), remove()都有著log(n)的時間複雜度。其具體演算法實現參照了《演算法導論》。

TreeMap_base.png

出於效能原因,TreeMap是非同步的(not synchronized),如果需要在多執行緒環境使用,需要程式設計師手動同步;或者通過如下方式將TreeMap包裝成(wrapped)同步的:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

紅黑樹是一種近似平衡的二叉查詢樹,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低那個的一陪。具體來說,紅黑樹是滿足如下條件的二叉查詢樹(binary search tree):

  1. 每個節點要麼是紅色,要麼是黑色。
  2. 根節點必須是黑色
  3. 紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色)。
  4. 對於每個節點,從該點至null(樹尾端)的任何路徑,都含有相同個數的黑色節點。

在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件3或條件4,需要通過調整使得查詢樹重新滿足紅黑樹的約束條件。

預備知識

前文說到當查詢樹的結構發生改變時,紅黑樹的約束條件可能被破壞,需要通過調整使得查詢樹重新滿足紅黑樹的約束條件。調整可以分為兩類:一類是顏色調整,即改變某個節點的顏色;另一類是結構調整,集改變檢索樹的結構關係。結構調整過程包含兩個基本操作:左旋(Rotate Left),右旋(RotateRight)

左旋

左旋的過程是將x的右子樹繞x逆時針旋轉,使得x的右子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateLeft.png

TreeMap中左旋程式碼如下:

右旋

右旋的過程是將x的左子樹繞x順時針旋轉,使得x的左子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查詢樹的屬性仍然滿足。

TreeMap_rotateRight.png

TreeMap中右旋程式碼如下:

尋找節點後繼

對於一棵二叉查詢樹,給定節點t,其後繼(樹種比大於t的最小的那個元素)可以通過如下方式找到:

  1. t的右子樹不空,則t的後繼是其右子樹中最小的那個元素。
  2. t的右孩子為空,則t的後繼是其第一個向左走的祖先。

後繼節點在紅黑樹的刪除操作中將會用到。

TreeMap_successor.png

TreeMap中尋找節點後繼的程式碼如下:

方法剖析

get()

get(Object key)方法根據指定的key值返回對應的value,該方法呼叫了getEntry(Object key)得到相應的entry,然後返回entry.value。因此getEntry()是演算法的核心。演算法思想是根據key的自然順序(或者比較器順序)對二叉查詢樹進行查詢,直到找到滿足k.compareTo(p.key) == 0entry

TreeMap_getEntry.png

具體程式碼如下:

put()

put(K key, V value)方法是將指定的key, value對新增到map裡。該方法首先會對map做一次查詢,看是否包含該元組,如果已經包含則直接返回,查詢過程類似於getEntry()方法;如果沒有找到則會在紅黑樹中插入新的entry,如果插入之後破壞了紅黑樹的約束條件,還需要進行調整(旋轉,改變某些節點的顏色)。

上述程式碼的插入部分並不難理解:首先在紅黑樹上找到合適的位置,然後建立新的entry並插入(當然,新插入的節點一定是樹的葉子)。難點是調整函式fixAfterInsertion(),前面已經說過,調整往往需要1.改變某些節點的顏色,2.對某些節點進行旋轉。

TreeMap_put.png

調整函式fixAfterInsertion()的具體程式碼如下,其中用到了上文中提到的rotateLeft()rotateRight()函式。通過程式碼我們能夠看到,情況2其實是落在情況3內的。情況4~情況6跟前三種情況是對稱的,因此圖解中並沒有畫出後三種情況,讀者可以參考程式碼自行理解。

remove()

remove(Object key)的作用是刪除key值對應的entry,該方法首先通過上文中提到的getEntry(Object key)方法找到key值對應的entry,然後呼叫deleteEntry(Entry<K,V> entry)刪除對應的entry。由於刪除操作會改變紅黑樹的結構,有可能破壞紅黑樹的約束條件,因此有可能要進行調整。

getEntry()函式前面已經講解過,這裡重點放deleteEntry()上,該函式刪除指定的entry並在紅黑樹的約束被破壞時進行呼叫fixAfterDeletion(Entry<K,V> x)進行調整。

由於紅黑樹是一棵增強版的二叉查詢樹,紅黑樹的刪除操作跟普通二叉查詢樹的刪除操作也就非常相似,唯一的區別是紅黑樹在節點刪除之後可能需要進行調整。現在考慮一棵普通二叉查詢樹的刪除過程,可以簡單分為兩種情況:

  1. 刪除點p的左右子樹都為空,或者只有一棵子樹非空。
  2. 刪除點p的左右子樹都非空。

對於上述情況1,處理起來比較簡單,直接將p刪除(左右子樹都為空時),或者用非空子樹替代p(只有一棵子樹非空時);對於情況2,可以用p的後繼s(樹中大於x的最小的那個元素)代替p,然後使用情況1刪除s(此時s一定滿足情況1.可以畫畫看)。

基於以上邏輯,紅黑樹的節點刪除函式deleteEntry()程式碼如下:

上述程式碼中佔據大量程式碼行的,是用來修改父子節點間引用關係的程式碼,其邏輯並不難理解。下面著重講解刪除後調整函式fixAfterDeletion()。首先請思考一下,刪除了哪些點才會導致調整?只有刪除點是BLACK的時候,才會觸發調整函式,因為刪除RED節點不會破壞紅黑樹的任何約束,而刪除BLACK節點會破壞規則4。

跟上文中講過的fixAfterInsertion()函式一樣,這裡也要分成若干種情況。記住,無論有多少情況,具體的調整操作只有兩種:1.改變某些節點的顏色,2.對某些節點進行旋轉。

TreeMap_fixAfterDeletion.png

上述圖解的總體思想是:將情況1首先轉換成情況2,或者轉換成情況3和情況4。當然,該圖解並不意味著調整過程一定是從情況1開始。通過後續程式碼我們還會發現幾個有趣的規則:a).如果是由情況1之後緊接著進入的情況2,那麼情況2之後一定會退出迴圈(因為x為紅色);b).一旦進入情況3和情況4,一定會退出迴圈(因為x為root)。

刪除後調整函式fixAfterDeletion()的具體程式碼如下,其中用到了上文中提到的rotateLeft()rotateRight()函式。通過程式碼我們能夠看到,情況3其實是落在情況4內的。情況5~情況8跟前四種情況是對稱的,因此圖解中並沒有畫出後四種情況,讀者可以參考程式碼自行理解。

TreeSet

前面已經說過TreeSet是對TreeMap的簡單包裝,對TreeSet的函式呼叫都會轉換成合適的TreeMap方法,因此TreeSet的實現非常簡單。這裡不再贅述。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

Java集合框架原始碼剖析:TreeSet 和 TreeMap

相關文章