今天嘗試刨一下TreeMap的祖墳。
底層結構對比
先來看一下與HashMap、LinkedHashMap和TreeMap的對比,同時就當是復習一下:
- HashMap使用數組存儲數據,并使用單向鏈表結構存儲hash沖突數據,同一個沖突桶中數據量大的時候(默認超過8)則使用紅黑樹存儲沖突數據。
- LinkedHashMap使用數組+雙向鏈表存儲數據,沖突數據存儲方式同HashMap。
- TreeMap使用紅黑樹存儲數據,注意是直接使用紅黑樹,不使用table數組。
關于排序特性
- HashMap無順序,不能保持順序。
- LinkedHashMap能保持寫入的順序,遍歷的時候可以按照寫入順序獲取數據。
- TreeMap是有序的Map,自動按照key值排序存儲,遍歷時獲取到的是有序數據。
需要注意LinkedHashMap和TreeMap在順序方面的區別,LinkedHashMap只能保持寫入順序,從“排序”的角度講,他實際是無序的。
只有TreeMap是可以實現自動排序的。
TreeMap按照什么排序?
TreeMap底層支持兩種排序方式:
- TreeMap對象實例化時傳入comparator對象。
- key值對象實現Comparable接口。
如果以上兩點都不能滿足的話,向TreeMap對象put數據的時候會拋出運行時異常。
比如TreeMap<String,Object>,由于String實現了Comparable接口,所以是沒有問題的。
但是如果自定義的對象,沒有實現Comparable接口,同時在TreeMap實例化的時候沒有設置comparator對象,則該TreeMap對象實際是不可用的。
TreeMap是否可以存儲null?
指的是,是否可以存儲key為空的數據?我們知道HashMap是可以支持唯一一個null對象的。
很多人都說不可以,但是我覺得有條件可以,但是沒做測試(因為感覺則個問題其實有點扯)。
條件是實例化TreeMap對象的時候指定comparator對象,同時,該comparator對象的compare方法可以支持null。
研究TreeMap的put源碼,也可以發現對以上說法的支持:
Comparator<? super K> cpr = comparator;if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();...省略若干代碼
可以發現如果有comparator的話,put方法不會立即拋出異常。但是如果comparator對象的compare方法不能支持null的話,一樣會拋出異常。
put方法
由于TreeMap支持自動排序,所以put方法會檢查是否滿足規則。
不滿足排序規則,拋出異常。
否則,按照紅黑樹算法規則要求,創建紅黑樹,存儲數據。
所以這里就涉及到一個重要的數據結構:紅黑樹。
二叉樹BST & 平衡二叉樹AVL
紅黑樹是樹結構的一種,是比二叉樹和平衡二叉樹更加復雜的一種數據結構。所以我們先從簡單的入手,了解一下二叉樹。
樹結構其實是實現了子排序的一種數據結構,我們說到的樹結構一般指的是二叉樹、也叫二叉搜索樹(BST - Binary Search Tree),定義:它或者是一棵空樹,或者是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小于它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值; 它的左、右子樹也分別為二叉排序樹。
二叉搜索樹的插入、搜索操作所花的時間都和樹的高度成正比。因此,如果共有n個元素,那么平均每次操作需要O(logn)的時間。
二叉搜索樹的特性并不能保證他是平衡的,也就是說,極端情況下,一顆二叉搜索樹從根節點開始,只有左節點、沒有右節點(比如一直按照從大到小或者相反順序插入二叉樹),這種情況下,二叉樹就蛻變成了鏈表,查詢時間復雜度就會下降為O(n)。
改善二叉樹這一缺點的經典數據結構是平衡二叉樹AVL(Adelson-Velsky and Landis Tree,以兩位發明者命名)。平衡二叉樹的特性:
1.本身首先是一棵二叉搜索樹。
2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1。
一顆平衡二叉樹在新節點插入之后可能會失衡,包括以下四種場景:
左左失衡LL
插入的數據在左側不平衡樹的左側,這種情況下需要進行右旋操作再次平衡:
我們需要記住一個概念:平衡二叉樹失衡之后通過旋轉動作使得它再次平衡的處理,只是針對失衡的子樹、不需要對整個樹進行操作。比如上圖新的節點1加入之后,導致pivot節點7一側的樹失衡,我們需要處理的是包含pivot節點的root節點的左側樹這一部分子樹,右側節點18下即使有再多的節點,也不需要處理。
右旋操作的實質是:以pivot節點7為支點做順時針旋轉,旋轉之后7變為自己原來父節點的父節點、7的左節點不變,7的右節點變為7原來的父節點的左節點。
右右失衡RR
插入的節點在右側不平衡樹的右側,這種情況下需要左旋:
左旋操作的實質是:以pivot節點18為支點做逆時針旋轉,旋轉之后18變為自己原來父節點的父節點、18的右節點不變,18的左節點變為18原來的父節點的右節點。
左右失衡LR
插入的新節點在左側不平衡樹的右側。先左旋再右旋
右左失衡LR
插入的節點在右側不平衡樹的左側,先右旋再左旋:
平衡二叉樹具有:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1 這一特性,這一嚴格條件會導致每次插入新節點之后總會大概率破壞平衡、從而必須要通過上述的旋轉操作使其再次達到平衡,旋轉操作會影響數據插入的效率。
紅黑樹可以解決這一問題。
紅黑樹
紅黑樹是一種帶顏色(節點是紅色或者黑色)的平衡二叉樹,具有以下特性:
性質1. 結點是紅色或黑色。
性質2. 根結點是黑色。
性質3. 所有葉子都是黑色。(葉子是NIL結點)
性質4. 每個紅色結點的兩個子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點)
性質5. 從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點。
紅黑樹新加入節點的顏色默認為黑色,因為根據紅黑樹性質4,每個紅色節點的兩個子節點都是黑色。所以,新加入節點如果是黑色的話,可以檢查其父節點如果是紅色的話,可以自動滿足性質4從而減少再平衡操作、提高數據加入的效率。這一點可以通過TreeMap的put方法源碼得到驗證:
public V put(K key, V value) {Entry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) checkroot = new Entry<>(key, value, null);size = 1;modCount++;return null;}int cmp;Entry<K,V> parent;// split comparator and comparable pathsComparator<? super K> cpr = comparator;if (cpr != null) {do {parent = t;cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}else {if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;do {parent = t;cmp = k.compareTo(t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;fixAfterInsertion(e);size++;modCount++;return null;}
put方法前半部分邏輯比較簡單,為新節點找到合適的位置加入,之后會調用fixAfterInsertion檢查是否破壞了紅黑樹的規則從而需要執行再平衡操作:
private void fixAfterInsertion(Entry<K,V> x) {x.color = RED;while (x != null && x != root && x.parent.color == RED) {if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {Entry<K,V> y = rightOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == rightOf(parentOf(x))) {x = parentOf(x);rotateLeft(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateRight(parentOf(parentOf(x)));}} else {Entry<K,V> y = leftOf(parentOf(parentOf(x)));if (colorOf(y) == RED) {setColor(parentOf(x), BLACK);setColor(y, BLACK);setColor(parentOf(parentOf(x)), RED);x = parentOf(parentOf(x));} else {if (x == leftOf(parentOf(x))) {x = parentOf(x);rotateRight(x);}setColor(parentOf(x), BLACK);setColor(parentOf(parentOf(x)), RED);rotateLeft(parentOf(parentOf(x)));}}}root.color = BLACK;}
fixAfterInsertion方法首先判斷其父節點是紅色的話,則不做任何操作。
get方法
根據紅黑樹查找算法查找并返回數據,紅黑樹是平衡二叉樹,查詢時間復雜度為O(log(n))。
key遍歷
比如調用TreeMap.keySet方法,采用遍歷二叉樹算法,按照從小到大的順序返回所有key值組成的循環器。
我該使用哪一個?
需要用到Map的時候,到底該使用哪一個的問題:
- 我只需要一個存儲數據的容器,沒有具體要求的話,用HashMap。
- 存儲數據后,有按照存儲順序獲取數據的需求,采用LinkedHashMap。
- 希望存儲數據的同時,幫助實現自動排序,采用TreeMap。
性能的問題,其實幾乎不需要考慮,不過我們還是需要知道:
- HashMap和LinkedHashMap查詢速度快,理想情況下時間復雜度幾乎是O(1)。
- HashMap寫入速度最快,LinkedHashMap寫入速度與HashMap幾乎相同,TreeMap寫入速度最慢(理論上,實際數據量小的情況下未必慢)。
- 遍歷速度相差無幾,理論上HashMap會慢一點,因為需要遍歷空桶。
并發問題尚待研究,但是我們清楚地知道,以上三種均不具備線程安全性。
好夢!