Java常用資料結構之Map(3)-TreeMap
之前公眾號釋出的文章中,《Java常用資料結構系列》漏了一章,就直接在掘金髮布了。
前言
TreeMap是一種帶有排序功能的key-value儲存結構,它是通過紅黑樹 實現的。如果想學習TreeMap的內部細節操作(旋轉平衡處理等),就必須充分學習紅黑樹。本文不關注紅黑樹操作的具體細節(大家自行補課),只分析TreeMap自身的特點。
整體結構
先來看看TreeMap的繼承關係:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable 複製程式碼
entrySet()
主要說一下NavigableMap介面:
public interface NavigableMap<K,V> extends SortedMap<K,V> 複製程式碼
繼承自SortedMap介面:
public interface SortedMap<K,V> extends Map<K,V> 複製程式碼
顧名思義,SortedMap的職責是排序,而NavigableMap的職責是在排好序的集合中進行各種導航搜尋的。
看SortedMap中的關鍵方法:
/** * Returns the comparator used to order the keys in this map, or * {@code null} if this map uses the {@linkplain Comparable * natural ordering} of its keys. * * @return the comparator used to order the keys in this map, *or {@code null} if this map uses the natural ordering *of its keys */ Comparator<? super K> comparator(); 複製程式碼
comparator()
方法就是返回比較器
的。從註釋中可以看出,有兩種排序方式:一種是自然排序(返回null),另一種則是自定義排序(返回Comparator例項)。
- 自然排序:要求Key必須實現Comparable介面,並且所有的Key都是同一個類的物件,否則會報ClassCastException異常。
- 自定義排序:需要實現一個Comparator比較器,不要求Key實現Comparable介面。
瀏覽一下NavigableMap中的部分導航方法。
// 返回小於key的第一個元素 Map.Entry<K,V> lowerEntry(K key); ... // 一系列類似方法 // 返回倒序集合 NavigableMap<K,V> descendingMap(); ... // 返回子集合,開閉區間 NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey,boolean toInclusive); ... // 一系列類似方法 複製程式碼
原始碼分析
核心紅黑樹
首當其衝的當然是用來儲存key-value鍵值對的儲存結構了。
// 直接用布林值來表示 private static final boolean RED= false; private static final boolean BLACK = true; static final class Entry<K,V> implements Map.Entry<K,V> { K key; // 鍵 V value; // 值 Entry<K,V> left; // 左子樹 Entry<K,V> right; // 右子樹 Entry<K,V> parent; // 父結點 boolean color = BLACK; // 標記是紅還是黑,預設黑色 複製程式碼
很明顯Entry是紅黑樹的樹結點結構,和HashMap中的TreeNode稍有區別。
然後就是紅黑樹的相關操作了,這裡僅簡單說明,不做展開。
// 左旋:左子樹不平衡時使用 private void rotateLeft(Entry<K,V> p) // 右旋:右子樹不平衡時使用 private void rotateRight(Entry<K,V> p) // 插入新結點 public V put(K key, V value) // 插入新結點後的調整,保證新樹還是紅黑樹 private void fixAfterInsertion(Entry<K,V> x) // 刪除某個結點 private void deleteEntry(Entry<K,V> p) // 刪除某個結點後的調整,保證新樹還是紅黑樹 private void fixAfterDeletion(Entry<K,V> x) 複製程式碼
這裡僅分析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(); @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; 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; } 複製程式碼
TreeMap的建構函式
// 使用自然排序 public TreeMap() // 使用自定義排序 public TreeMap(Comparator<? super K> comparator) // 傳的map不一定是有序的,所以呼叫的是putAll方法來進行新增 public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); } // 傳的map是有序的,需要做一些調整 public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException | ClassNotFoundException cannotHappen) { } } 複製程式碼
第三個和第四個構造方法的實現是不同的。在第三個構造方法中,不能保證傳入的Map是有序的,所以需要呼叫putAll方法將元素一個一個新增到Map中。而第四個構造方法中,傳入的就是一個有序的Map,所以直接將傳入的Map轉成紅黑樹了。
private void buildFromSorted(int size, Iterator<?> it, java.io.ObjectInputStream str, V defaultVal) throwsjava.io.IOException, ClassNotFoundException { this.size = size; // 將轉換後的樹的根結點賦值給TreeMap的根結點 // computeRedLevel可以理解為計算樹的高度 root = buildFromSorted(0, 0, size-1, computeRedLevel(size), it, str, defaultVal); } private final Entry<K,V> buildFromSorted(int level, int lo, int hi, int redLevel, Iterator<?> it, java.io.ObjectInputStream str, V defaultVal) throwsjava.io.IOException, ClassNotFoundException { if (hi < lo) return null; int mid = (lo + hi) >>> 1; // 取中間位置 // 遞迴左子樹 Entry<K,V> left= null; if (lo < mid) left = buildFromSorted(level+1, lo, mid - 1, redLevel, it, str, defaultVal); // extract key and/or value from iterator or stream K key; V value; if (it != null) { if (defaultVal==null) { Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next(); key = (K)entry.getKey(); value = (V)entry.getValue(); } else { key = (K)it.next(); value = defaultVal; } } else { // use stream key = (K) str.readObject(); value = (defaultVal != null ? defaultVal : (V) str.readObject()); } Entry<K,V> middle =new Entry<>(key, value, null); // color nodes in non-full bottommost level red if (level == redLevel) // 最底層的結點設成紅色 middle.color = RED; if (left != null) { middle.left = left; left.parent = middle; } // 遞迴右子樹 if (mid < hi) { Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel, it, str, defaultVal); middle.right = right; right.parent = middle; } return middle; // 返回根結點 } 複製程式碼
這個轉換方法是通過遞迴來將所有結點關聯成一個紅黑樹的,且會返回根結點(其實就是中間點)。有意思的是,它只將最底層的結點設定成了紅色,而其他結點都是黑色。這樣是為了方便後續結點的插入。
TreeMap的PrivateEntryIterator
TreeMap中所有迭代器子類都繼承自PrivateEntryIterator
:
abstract class PrivateEntryIterator<T> implements Iterator<T> { Entry<K,V> next; Entry<K,V> lastReturned; int expectedModCount; PrivateEntryIterator(Entry<K,V> first) { expectedModCount = modCount; lastReturned = null; next = first; } public final boolean hasNext() { return next != null; } // 下一個結點 final Entry<K,V> nextEntry() { ... next = successor(e); // 二叉樹查詢,主要查右子樹 lastReturned = e; return e; } // 前一個結點 final Entry<K,V> prevEntry() { ... next = predecessor(e); // 二叉樹查詢,主要查左子樹 lastReturned = e; return e; } public void remove() { ... } } 複製程式碼
直接看successor
方法,predecessor
方法類似。
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { if (t == null) return null; else if (t.right != null) { // 右子樹不為空,即存在比當前結點大的結點 Entry<K,V> p = t.right; while (p.left != null) // 這裡就需要查左子樹了 p = p.left; return p; } else { // 右子樹為空 Entry<K,V> p = t.parent; Entry<K,V> ch = t; while (p != null && ch == p.right) { // 針對葉結點 ch = p; p = p.parent; } // 因為結點t可能是其父節點的左子樹,也可能是右子樹 return p; } } 複製程式碼
因為繼承了AbstractMap,所以必須實現entrySet()
方法:
public Set<Map.Entry<K,V>> entrySet() { EntrySet es = entrySet; return (es != null) ? es : (entrySet = new EntrySet()); } class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { // 把紅黑樹的最小結點作為迭代器的第一個結點 return new EntryIterator(getFirstEntry()); } ... } final class EntryIterator extends PrivateEntryIterator<Map.Entry<K,V>> { EntryIterator(Entry<K,V> first) { super(first); } public Map.Entry<K,V> next() { return nextEntry(); } } 複製程式碼
EntrySet繼承了AbstractSet,其中iterator()
方法返回了EntryIterator
,它直接就繼承了PrivateEntryIterator
介面。類似的迭代器還有:ValueIterator
、KeyIterator
和DescendingKeyIterator
。
TreeMap中的導航方法
TreeMap中有很多導航方法,比如:lowerEntry
、lowerKey
、tailMap
等等,方法本身實現沒有什麼要說的。如果你仔細閱讀原始碼,你會發現有下面這兩種方法(還有類似的):
public Map.Entry<K,V> lowerEntry(K key) { return exportEntry(getLowerEntry(key)); } final Entry<K,V> getLowerEntry(K key) { ... } 複製程式碼
為什麼給出兩個方法?明明getLowerEntry
就可以拿到Entry了。其實,lowerEntry
才是對外介面,而getLowerEntry
是內部介面。因為getLowerEntry
拿到的Entry是可讀寫的,而TreeMap不希望開發人員修改返回的Entry,所以多做了一層處理,讓返回的Entry只能讀。關鍵在exportEntry
方法:
static <K,V> Map.Entry<K,V> exportEntry(TreeMap.Entry<K,V> e) { return (e == null) ? null : new AbstractMap.SimpleImmutableEntry<>(e); } 複製程式碼
可以看到,直接強轉成了SimpleImmutableEntry
,它是AbstractMap實現的一個不可變Entry,它的setValue
方法會丟擲UnsupportedOperationException
異常。
反向TreeMap
TreeMap由紅黑樹實現,它是有序的,所以它可以反向:
private transient NavigableMap<K,V> descendingMap; public NavigableMap<K, V> descendingMap() { NavigableMap<K, V> km = descendingMap; return (km != null) ? km : (descendingMap = new DescendingSubMap<>(this, true, null, true, true, null, true)); } 複製程式碼
那如何反向呢?
static final class DescendingSubMap<K,V>extends NavigableSubMap<K,V> { ... // 直接反轉比較器 private final Comparator<? super K> reverseComparator = Collections.reverseOrder(m.comparator); ... } 複製程式碼
在DescendingSubMap
中,可以發現,所謂反向其實只需要反轉比較器就可以了。
既然可以反向,那TreeMap就可以進行逆序遍歷和迭代。