一、前言
當我們需要把插入的元素進行排序的時候,就是時候考慮TreeMap了,從名字上來看,TreeMap肯定是和樹是脫不了干係的,它是一個排序了的Map,下面我們來著重分析其原始碼,理解其底層如何實現排序功能。下面,開始分析。
二、TreeMap示例
import java.util.TreeMap; import java.util.Map; public class TreeMapTest { public static void main(String[] args) { Map<String, String> maps = new TreeMap<String, String>(); maps.put("aa", "aa"); maps.put("cc", "cc"); maps.put("bb", "bb"); for (Map.Entry<String, String> entry : maps.entrySet()) { System.out.println(entry.getKey() + " : " + entry.getValue()); } } }
執行結果:
aa : aa
bb : bb
cc : cc
說明:從輸出結果可以看到TreeMap對插入的元素進行了排序。
三、TreeMap資料結構
TreeMap底層使用的資料結構是紅黑樹,有印象的的讀者,應該知道我們在分析HashMap的時候就已經接觸到了紅黑樹結構,只是沒有對紅黑樹進行詳細的分析,現在,筆者也並不打算對紅黑樹做太過仔細的分析,因為筆者之後會出資料結構的專題(先挖個坑),到時候再來一睹各種資料結構的風采。
說明:上圖為典型的紅黑樹結構,效率很高,具體的細節問題,我們以後詳談。
四、TreeMap原始碼分析
4.1 類的繼承關係
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
說明:繼承了抽象類AbstractMap,AbstractMap實現了Map介面,實現了部分方法。不能進行例項化,實現了NavigableMap,Cloneable,Serializable介面,其中NavigableMap是繼承自SortedMap的介面,定義了一系列規範。
4.2 類的屬性
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { // 比較器,用於控制Map中的元素順序 private final Comparator<? super K> comparator; // 根節點 private transient Entry<K,V> root; // 樹中結點個數 private transient int size = 0; // 對樹進行結構性修改的次數 private transient int modCount = 0; }
說明:重點是比較器Comparator,此介面實現了對插入元素進行排序。
4.3 類的建構函式
1. TreeMap()型建構函式
public TreeMap() { // 無使用者自定義比較器 comparator = null; }
2. TreeMap(Comparator<? super K>)型建構函式
// 自定義了比較器 public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
說明:使用者自定義了比較器,可以按照使用者的邏輯進行比較,確定元素的訪問順序。
3. TreeMap(Map<? extends K, ? extends V>)型建構函式
// 從已有map中構造TreeMap public TreeMap(Map<? extends K, ? extends V> m) { comparator = null; putAll(m); }
說明:根據已有的Map構造TreeMap。
4. TreeMap(SortedMap<K, ? extends V>)型建構函式
// 從SortedMap中構造TreeMap,有比較器 public TreeMap(SortedMap<K, ? extends V> m) { comparator = m.comparator(); try { buildFromSorted(m.size(), m.entrySet().iterator(), null, null); } catch (java.io.IOException cannotHappen) { } catch (ClassNotFoundException cannotHappen) { } }
說明:傳入SortedMap型引數,實現SortedMap介面的類都會實現comparator方法,用於返回比較器。
4.4 核心函式分析
1. put函式
public V put(K key, V value) { // 記錄根節點 Entry<K,V> t = root; // 根節點為空 if (t == null) { // 比較key compare(key, key); // type (and possibly null) check // 新生根節點 root = new Entry<>(key, value, null); // 大小加1 size = 1; // 修改次數加1 modCount++; return null; } int cmp; Entry<K,V> parent; // 獲取比較器 Comparator<? super K> cpr = comparator; // 比較器不為空 if (cpr != null) { // 找到元素合適的插入位置 do { // parent賦值 parent = t; // 比較key與元素的key值,在Comparator類的compare方法中可以實現我們自己的比較邏輯 cmp = cpr.compare(key, t.key); // 小於結點key值,向左子樹查詢 if (cmp < 0) t = t.left; // 大於結點key值,向右子樹查詢 else if (cmp > 0) t = t.right; // 表示相等,直接更新結點的值 else return t.setValue(value); } while (t != null); } // 比較器為空 else { // key為空,丟擲異常 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") // 取得K實現的比較器 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); // 大小加1 size++; // 進行了結構性修改 modCount++; return null; }
說明:插入一個元素時,若使用者自定義比較器,則會按照使用者自定義的邏輯確定元素的插入位置,否則,將會使用K自身實現的比較器確定插入位置。
2. getEntry函式
final Entry<K,V> getEntry(Object key) { // 判斷比較器是否為空 if (comparator != null) // 根據自定義的比較器來返回結果 return getEntryUsingComparator(key); // 比較器為空 // key為空,丟擲異常 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") // 取得K自身實現了比較介面 Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; // 根據Comparable介面的compareTo函式來查詢元素 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; }
說明:當我們呼叫get函式時,實際上是委託getEntry函式獲取元素,對於使用者自定義實現的Comparator比較器而言,是使用getEntryUsingComparator函式來完成獲取邏輯。
具體程式碼如下
final Entry<K,V> getEntryUsingComparator(Object key) { @SuppressWarnings("unchecked") // 向下轉型 K k = (K) key; // 取得比較器 Comparator<? super K> cpr = comparator; // 比較器不為空 if (cpr != null) { Entry<K,V> p = root; // 開始遍歷樹節點找到對應的結點 while (p != null) { int cmp = cpr.compare(k, p.key); // 小於結點key值,向左子樹查詢 if (cmp < 0) p = p.left; // 大於結點key值,向右子樹查詢 else if (cmp > 0) p = p.right; // 相等,找到,直接返回 else return p; } } return null; }
說明:會根據使用者定義在compare函式裡面的邏輯進行元素的查詢。
3. deleteEntry函式
private void deleteEntry(Entry<K,V> p) { // 結構性修改 modCount++; // 大小減1 size--; // p的左右子結點均不為空 if (p.left != null && p.right != null) { // 找到p結點的後繼 Entry<K,V> s = successor(p); // 將p的值用其後繼結點的key-value替換,並且用s指向其後繼 p.key = s.key; p.value = s.value; p = s; } // 開始進行修正,具體的修正過程我們會在之後的資料結構專區進行講解 // 現在可以看成是為了保持紅黑樹的特性,提高效能 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; } } }
說明:deleteEntry函式會在remove函式中被呼叫,它完成了移除元素的主要工作,刪除該結點後會對紅黑樹進行修正,此部分內容以後會詳細講解,同時,在此函式中需要呼叫successor函式,即找到該結點的後繼結點。具體函式程式碼如下
// 找到後繼 static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { // t為null,直接返回null 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 { // 右孩子為空 // 儲存t的父節點 Entry<K,V> p = t.parent; // 儲存t結點 Entry<K,V> ch = t; // 進行回溯,找到後繼,直到p == null || ch != p.right while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }
說明:當結點的右子樹為空的時候,進行回溯可以找到該結點的後繼結點。
五、問題擴充套件
1. 如何找到小於指定結點的最大結點?參考getLowerEntry函式原始碼
2. 如何找到大於指定結點的最小結點?參考getHigherEntry函式原始碼
對getLowerEntry原始碼分析如下
final Entry<K,V> getLowerEntry(K key) { // 儲存根節點 Entry<K,V> p = root; // 根節點不為空 while (p != null) { // 比較該key與節點的key int cmp = compare(key, p.key); if (cmp > 0) { // 如果該key大於結點的key // 如果結點的右子樹不為空,與該結點右結點進行比較 if (p.right != null) p = p.right; else // 右子樹為空,則直接返回結點;因為此時已經沒有比該結點key更大的結點了(右子樹為空) return p; } else { // 如果該key小於等於結點的key // 結點的左子樹不為空,與該結點的左結點進行比較 if (p.left != null) { p = p.left; } else { // 結點的左子樹不為空,則開始進行回溯 Entry<K,V> parent = p.parent; Entry<K,V> ch = p; while (parent != null && ch == parent.left) { ch = parent; parent = parent.parent; } return parent; } } } return null; }
流程圖如下:
getHigherEntry則可以以此類推。
六、總結
由TreeMap我們可以知道其底層的資料結構為紅黑樹,並且可以使用使用者自定義的比較器來實現比較邏輯。對於其核心函式的分析就到此為止了,謝謝各位園友的觀看~