TreeMap分析(上)
因為TreeMap的相關知識較多,故TreeMap的分析將會分成三篇文章來寫,望大家諒解。
本篇文章先給大家介紹一下紅黑樹基本概念,並分析一下在紅黑樹中查找某個結點的相關源碼實現。
TreeMap是啥
從TreeMap的類名上就能知道它的底層存儲結構其實是紅黑樹。首先簡單介紹一下紅黑樹的相關知識,以便理解後續內容。
什麽是紅黑樹?先放一張紅黑樹的示意圖看看:
註:圖片出處為 博客園 —— 五月的倉頡
簡單解釋一下樹的相關術語的含義:
1.根結點(即0026結點):整個樹結構圖中最上面的一個結點,其他所有的結點都是由該結點延伸出去的。
2.父結點:在樹的上下結構中,有連接關系並處於上方的結點。比如0026是0017和0041的父結點,0017是0012和0021的父結點等等。
3.子結點:在樹的上下結構中,有連接關系並處於下方的結點。比如0017是0026的左子結點,0041是0026的右子結點;0030是0041的左子結點,0047是0041的右子結點。
並且左子節點是小於父結點的,而右子節點一般是大於父結點的。比如0012比0017小,而0010比0012小;同樣0041比0026大,並且0047也比0041大。
4.兄弟結點:屬於同一個父結點的結點互相稱為兄弟結點。比如0017和0041同屬於0026的子結點,則0017和0041就是兄弟節點。
5.葉子結點:沒有任何子結點的結點稱為葉子節點。如上圖中所有的NULL LEAF結點都是葉子結點。
6.二叉樹:每個節點最多只有兩個子結點的樹結構稱為二叉樹。如上圖就是一種二叉樹的數據結構。
樹的一些基本術語就介紹到這裏,其他的請同學們自行查閱資料學習吧,這裏就不再多說了 : )
什麽是紅黑樹
上面已經說了TreeMap是基於紅黑樹的存儲結構,那什麽是紅黑樹呢?
紅黑樹是一種特殊的二叉樹,它的每個結點都額外有一個顏色的屬性,顏色只有兩種:紅色和黑色。
關於紅黑樹有以下幾個規定:
1.首先當前樹必須為二叉樹的結構。
2.每個結點都有一種顏色,且顏色只能為紅色或黑色
3.根結點必須是黑色。
4.葉子結點必須是黑色。
5.不能同時出現父結點和子結點都是紅色的情況。即如果一個結點是父結點且為紅色,則它的子結點必須全部為黑色。
6.從根結點到每個葉子結點經過的路程中,黑色結點的數量必須是一樣的。
比如 0026 -> 0017 -> 0012 -> 0010 -> 0003 -> 葉子結點,這條路徑上共有4個黑色結點;
0026 -> 0041 -> 0030 -> 0038 -> 葉子結點,這條路徑上也有4個黑色結點。
只有以上6個條件全部滿足的樹,我們才稱其為紅黑樹。比如上圖的中的樹結構就是一個標準的紅黑樹。
TreeMap源碼中的數據結構及相關屬性
1 /** 2 * 紅黑樹每個結點對象的數據結構 3 * 4 * 5 * @param <K> key 6 * @param <V> value 7 */ 8 static final class Entry<K,V> implements Map.Entry<K,V> { 9 K key; //鍵 10 V value; //值 11 Entry<K,V> left; //左子結點引用 12 Entry<K,V> right; //右子結點引用 13 Entry<K,V> parent; //父結點引用 14 boolean color = BLACK; //結點默認為黑色 15 16 /** 17 * 構造一個葉子結點,默認無左右子結點,顏色為黑色 18 */ 19 Entry(K key, V value, Entry<K,V> parent) { 20 this.key = key; 21 this.value = value; 22 this.parent = parent; 23 } 24 25 /** 26 * 返回key 27 * 28 * @return the key 29 */ 30 public K getKey() { 31 return key; 32 } 33 34 /** 35 * 返回key對應的value 36 * 37 * @return the value associated with the key 38 */ 39 public V getValue() { 40 return value; 41 } 42 43 /** 44 * 替換對應的值 45 * 46 * @return 原先被替換的值 47 */ 48 public V setValue(V value) { 49 V oldValue = this.value; 50 this.value = value; 51 return oldValue; 52 } 53 54 /** 55 * 重寫結點的equals()方法,比較結點的大小時使用 56 */ 57 public boolean equals(Object o) { 58 if (!(o instanceof Map.Entry)) 59 return false; 60 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 61 //必須key和value都相同才算相等 62 return valEquals(key,e.getKey()) && valEquals(value,e.getValue()); 63 } 64 65 /** 66 * 重寫結點的hashCode()方法 67 */ 68 public int hashCode() { 69 int keyHash = (key==null ? 0 : key.hashCode()); 70 int valueHash = (value==null ? 0 : value.hashCode()); 71 return keyHash ^ valueHash; //key的hashCode 異或 value的hashCode 72 } 73 74 /** 75 * 重寫toString()方法 76 */ 77 public String toString() { 78 return key + "=" + value; 79 } 80 } 81 82 /** 用戶自定義的比較器 */ 83 private final Comparator<? super K> comparator; 84 85 /** 紅黑樹根結點 */ 86 private transient Entry<K,V> root; 87 88 /** 紅黑樹總結點個數 */ 89 private transient int size = 0;
可以看到TreeMap因為實現了Map的結點的數據結構,所以同樣有key和value兩個屬性。並且因為是紅黑樹,所以還有父結點、左右子結點的引用以及結點顏色這些屬性。
我們看到第83行有個 comparator 屬性,它表示當前TreeMap使用的是哪種比較器。
大家都知道TreeMap是支持結點排序的,它有兩種排序方式:
1.按照結點key的自然順序維護結點的順序。比如有兩個結點 A Entry<1,"11"> 和 B Entry<2,"222">,以這種key的自然排序來講,如果先插入A後插入B,則A為B父結點,B為A的右子結點;如果先插入B後插入A,那麽B為A的父結點,A為B的左子結點。
2.另一種排序是用戶自定義的排序方式。在構造方法中傳入一個自定義的比較器,則TreeMap會根據用戶自己定義的結點對象比較大小的方法來維護結點的順序。
TreeMap構造方法
1 /** 2 * 無參數構造方法,使用key的自然比較順序來維護樹中結點的順序 3 */ 4 public TreeMap() { 5 comparator = null; 6 } 7 8 /** 9 * 帶參數構造方法,使用用戶傳入的比較器來維護樹中結點的順序 10 * @param 用戶自定義比較器,如果為空,則該Map會使用key的自然排序維護樹的順序 11 */ 12 public TreeMap(Comparator<? super K> comparator) { 13 this.comparator = comparator; 14 }
TreeMap獲取某個結點的源碼分析
1 /** 2 * 獲取節點的操作 3 * 如果在map中找到了這個鍵,則返回鍵對應的值;找不到對應的鍵或者鍵指向為null,否則返回null 4 * 5 * @throws ClassCastException 用戶傳入的key和當前map中的key無法比較(不是同一類型),則報該異常 6 * @throws NullPointerException 使用了自然排序但傳入的key為null,或者比較器不支持key為null的情況,則報此異常 7 * 8 */ 9 public V get(Object key) { 10 Entry<K,V> p = getEntry(key); //可以看到實際上是調用getEntry()這個方法來獲取節點的 11 return (p==null ? null : p.value); 12 } 13 14 15 /** 16 * 返回通過傳入的鍵在map中找到的相應entry,若未找到則返回null 17 * 18 * @throws ClassCastException 用戶傳入的key和當前map中的key無法比較(不是同一類型),則報該異常 19 * @throws NullPointerException 使用了自然排序但傳入的key為null,或者比較器不支持key為null的情況,則報此異常 20 */ 21 final Entry<K,V> getEntry(Object key) { 22 // 首先要區分是使用map鍵的自然排序查找還是使用用戶自定義的比較器來進行查找 23 if (comparator != null) //如果用戶傳入了自定義比較器 24 return getEntryUsingComparator(key); //調用getEntryUsingComparator()方法,通過用戶自定義比較器的compare()方法查找entry 25 if (key == null) //如果key為null,報空指針異常 26 throw new NullPointerException(); 27 Comparable<? super K> k = (Comparable<? super K>) key; //沒有傳入自定義比較器,使用key的自然排序比較傳入的key和map中的key 28 Entry<K,V> p = root; //從根節點開始比較 29 while (p != null) { //如果節點不是空,則一直循環遍歷比較 30 int cmp = k.compareTo(p.key); //獲取傳入的key和當前節點key的比較結果,使用自然排序Comparable實現的compareTo()方法進行比較 31 if (cmp < 0) //結果<0,說明傳入的key比當前節點的key小 32 p = p.left; //將下次比較的節點變更為當前節點的左子節點 33 else if (cmp > 0) //結果>0,說明傳入的key比當前節點的key大 34 p = p.right; //將下次比較的節點變更為當前節點的右子節點 35 else //結果相等,則該節點就是要查找的節點 36 return p; //直接返回節點對應的Entry 37 } 38 return null; //遍歷完整個樹也沒找到,則返回null 39 } 40 41 42 43 /** 44 * 使用用戶自定義比較器的比較方法來查找節點 45 * 邏輯與通過自然排序查找類似,不再贅述 46 */ 47 final Entry<K,V> getEntryUsingComparator(Object key) { 48 @SuppressWarnings("unchecked") 49 K k = (K) key; 50 Comparator<? super K> cpr = comparator; 51 if (cpr != null) { 52 Entry<K,V> p = root; 53 while (p != null) { 54 int cmp = cpr.compare(k, p.key); //僅僅這裏和getEntry()方法不同,使用自定義比較器的compare()方法進行比較 55 if (cmp < 0) 56 p = p.left; 57 else if (cmp > 0) 58 p = p.right; 59 else 60 return p; 61 } 62 } 63 return null; 64 }
可以看到查找其實很簡單,就是從根結點開始往下遍歷比較。如果要查找的節點比較小,則繼續往左子結點比較;反之則往右子結點比較;如果相等,則當前結點就是要查找的結點。
要註意的只有一點,即TreeMap使用的是key的自然排序還是用戶自定義的排序。
key自然排序使用的是實現了Comparable接口的compareTo()方法來進行比較,而用戶自定義排序則使用的實現Comparator接口的compare()方法來比較。
這裏延伸一個經典的面試題:Comparable接口和Comparator接口有啥相同和不同之處?
相同點:兩個接口都是用於支持對象進行比較大小所定義的。
不同點:
1.Comparable具體實現比較大小的方法是CompareTo(),而Comparator具體實現比較大小的方法是Compare()。
2.Comparable一般強調相同類型的對象進行比較(比如小明和小紅都是人類,那他們之間的大小就是按年齡大小來進行比較的,那麽CompareTo()方法中其實就是兩人的age進行比較);
而相對的Comparator一般強調不同類型的對象進行比較(比如小華是人類,旺財是狗,而人類要和狗進行比較大小,則必須由用戶自己制定一個比較規則來確定誰大誰小,那麽compare()方法中其實就是自定義的兩個類型的不同屬性進行比較)。
明確這兩個接口的區別後,上面查詢結點的方法也就不難理解啦 : )
本篇文章到此結束,內容還是偏基礎,希望大家不吝賜教。
那麽下篇文章我會和大家一起分析下在插入節點後,紅黑樹內部數據結構是怎樣一步步變化的,敬請期待喲~
TreeMap分析(上)