Map的單元是對鍵值對的處理,之前分析過的兩種Map,HashMap和LinkedHashMap都是用哈希值去尋找我們想要的鍵值對,優點是由O(1)的查找速度。
那如果我們在一個對查找性能要求不那么高,反而對有序性要求比較高的應用場景呢?
這個時候HashMap就不再適用了,我們需要一種新的Map,在JDK中提供了一個接口:SortedMap,我想分析一下具體的實現中的一種:TreeMap.
HahMap是Key無序的,而TreeMap是Key有序的。
1.看一下基本成員:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { private final Comparator<? super K> comparator; private transient Entry<K,V> root = null; private transient int size = 0; private transient int modCount = 0; public TreeMap() { comparator = null; } public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } //后面省略 }
TreeMap繼承了NavigableMap,而NavigableMap繼承自SortedMap,為SortedMap添加了搜索選項,NavigableMap有幾種方法,分別是不同的比較要求:floorKey是小於等於,ceilingKey是大於等於,lowerKey是小於,higherKey是大於。
注意初始化的時候,有一個Comparator成員,這是用於維持有序的比較器,當我們想做一個自定義數據結構的TreeMap時,可以重寫這個比較器。
2.我們看一下Entry的成員:
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left = null; Entry<K,V> right = null; Entry<K,V> parent; boolean color = BLACK; //后續省略 }
咦?木有了熟悉了哈希值,多了left,right,parent,這是我們的樹結構,最后看到color,明白了:TreeMap是基於紅黑樹實現的!而且默認的節點顏色是黑色。
至於紅黑樹,想必多多少少都聽過,這是一種平衡的二叉查找樹,是2-3樹的一種變體,即擁有二叉查找樹的高效查找,擁有2-3樹的高效平衡插入能力。
紅黑樹巧妙的增加了顏色這個維度,對2-3樹的樹本身進行了降維成了二叉樹,這樣樹的調整不會再如2-3樹那么繁瑣。
有的同學看到這里會質疑我,你這個胡說八道,和算法導論里講的不一樣!
對,CLRS中確實沒有這段,這段選自《Algorithms》,我覺得提供了一種有趣的理解思路,所以如果之前只看了CLRS,建議去看一下這本書,互相驗證。
不過為了尊重JDK的作者,后面的還是按照CLRS中的講解來吧,畢竟在JDK源碼的注釋中寫着:From CLR。
我們在紅黑樹中的一切插入和刪除后,為了維護樹的有序性的動作看起來繁復,但都是為了維護下面幾個紅黑樹的基本性質:
(1)樹的節點只有紅與黑兩種顏色
(2)根節點為黑色的
(3)葉子節點為黑色的
(4)紅色節點的字節點必定是黑色的
(5)從任意一節點出發,到其后繼的葉子節點的路徑中,黑色節點的數目相同
紅黑樹的第4條性質保證了這些路徑中的任意一條都不存在連續的紅節點,而紅黑樹的第5條性質又保證了所有的這些路徑上的黑色節點的數目相同。因而最短路徑必定是只包含黑色節點的路徑,而最長路徑為紅黑節點互相交叉的路徑,由於所有的路徑的起點必須是黑色的,而紅色節點又不能連續存在,因而最長路徑的長度為全為黑色節點路徑長度的二倍。
回到TreeMap本身,看看它的put方法:

public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths 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; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); 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; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }
此處就是二叉樹的比較查找到合適的位置,然后插入,需要注意的是
(1)先檢測root節點是不是null,如果為null,則新插入的節點為root節點。
(2)最好自定義自己的Comparator,否則將會繼承原始的比較方法,可能會出現問題
(3)插入的鍵值不能為null,否則會拋出空指針的異常。
(4)插入新節點后,調用fixAfterInsertion(e)方法來修復紅黑樹。
看一下get方法,這里會調用getEntry方法,就是二叉查找樹的查找:

final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; }
還有一個remove方法,這里最后調用的是deleteEntry()方法,在deleteEntry()方法中最后調用fixAfterDeletion方法來修復樹的順序。
紅黑樹的刪除操作復雜的讓人發指,對着CLRS慢慢看吧:
public V remove(Object key) { Entry<K,V> p = getEntry(key); if (p == null) return null; V oldValue = p.value; deleteEntry(p); return oldValue; }

private void deleteEntry(Entry<K,V> p) { modCount++; size--; // If strictly internal, copy successor's element to p and then make p // point to successor. if (p.left != null && p.right != null) { Entry<K,V> 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<K,V> replacement = (p.left != null ? p.left : p.right); if (replacement != null) { // Link replacement to parent replacement.parent = p.parent; if (p.parent == null) root = replacement; else if (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; // Fix replacement if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { // return if we are the only node. root = null; } else { // No children. Use self as phantom replacement and unlink. if (p.color == BLACK) fixAfterDeletion(p); if (p.parent != null) { if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } }
上面所做的一切繁瑣操作都是為了紅黑樹的基本性質,而修復順序的操作中最基本的就是左旋和右旋了,下面是左旋和右選的源碼。
/** From CLR */ private void rotateLeft(Entry<K,V> p) { if (p != null) { Entry<K,V> r = p.right; p.right = r.left; if (r.left != null) r.left.parent = p; r.parent = p.parent; if (p.parent == null) root = r; else if (p.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; } } /** From CLR */ private void rotateRight(Entry<K,V> p) { if (p != null) { Entry<K,V> l = p.left; p.left = l.right; if (l.right != null) l.right.parent = p; l.parent = p.parent; if (p.parent == null) root = l; else if (p.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; } }
其實所有的操作都是關於紅黑樹的操作,
決定了TreeMap的有序性,對於TreeMap的增刪改查的效率都是O(Log(n))的。
到這里,TreeMap其實就差不多了,最關鍵的還是對紅黑樹的操作,希望這種數據結構的知識能掌握的比較扎實吧,多看書,多編程,夯實基礎,與諸君共勉。