1. 程式人生 > >TreeMap分析(上)

TreeMap分析(上)

roo 二叉樹 內部數據 指向 param 基礎 分享 知識 tree

因為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分析(上)