java集合類TreeMap和TreeSet

看這篇博客前,能夠先看下下列這幾篇博客java

 

 
 
TreeMap 和 TreeSet 是 Java Collection Framework 的兩個重要成員,其中 TreeMap 是 Map 接口的經常使用實現類,而 TreeSet 是 Set 接口的經常使用實現類。雖然 TreeMap 和 TreeSet 實現的接口規範不一樣,但 TreeSet 底層是經過 TreeMap 來實現的(如同HashSet底層是是經過HashMap來實現的同樣),所以兩者的實現方式徹底同樣。而 TreeMap 的實現就是紅黑樹算法。
 
 

1. TreeSet和TreeMap的關係

-----------------------------------------------------
 
與HashSet徹底相似,TreeSet裏面絕大部分方法都市直接調用TreeMap方法來實現的。
 
相同點:
  1. TreeMap和TreeSet都是有序的集合,也就是說他們存儲的值都是拍好序的。
  2. TreeMap和TreeSet都是非同步集合,所以他們不能在多線程之間共享,不過可使用方法Collections.synchroinzedMap()來實現同步
  3. 運行速度都要比Hash集合慢,他們內部對元素的操做時間複雜度爲O(logN),而HashMap/HashSet則爲O(1)。
不一樣點:
  1. 最主要的區別就是TreeSet和TreeMap非別實現Set和Map接口
  2. TreeSet只存儲一個對象,而TreeMap存儲兩個對象Key和Value(僅僅key對象有序)
  3. TreeSet中不能有重複對象,而TreeMap中能夠存在
 
理解了這些以後咱們發現其實二者底層的實現方法仍是一致的,因此下面咱們只須要分析TreeMap,基本上就弄懂了TreeSet。
 
 

2. TreeSet實現原理

-------------------------------------------------------
 
TreeMap 的實現使用了紅黑樹數據結構,也就是一棵自平衡的排序二叉樹,這樣就能夠保證快速檢索指定節點。對於 TreeMap 而言,它採用一種被稱爲「紅黑樹」的排序二叉樹來保存 Map 中每一個 Entry —— 每一個 Entry 都被當成「紅黑樹」的一個節點對待。舉例:
[java] view plain copy
 
print?在CODE上查看代碼片派生到個人代碼片
  1. public class TreeMapTest  {     
  2.     public static void main(String[] args) {     
  3.         TreeMap<String , Double> map =  new TreeMap<String , Double>();     
  4.         map.put("ccc" , 89.0);     
  5.         map.put("aaa" , 80.0);     
  6.         map.put("zzz" , 80.0);     
  7.         map.put("bbb" , 89.0);     
  8.         System.out.println(map);     
  9.     }     
  10. }  
 
 

當程序執行 map.put("ccc" , 89.0); 時,系統將直接把 "ccc"-89.0 這個 Entry 放入 Map 中,這個 Entry 就是該「紅黑樹」的根節點。接着程序執行 map.put("aaa" , 80.0); 時,程序會將 "aaa"-80.0 做爲新節點添加到已有的紅黑樹中。算法

之後每向 TreeMap 中放入一個 key-value 對,系統都須要將該 Entry 當成一個新節點,添加成已有紅黑樹中,經過這種方式就可保證 TreeMap 中全部 key 老是由小到大地排列。例如咱們輸出上面程序,將看到以下結果(全部 key 由小到大地排列):數組

 {aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0}
 
 
TreeMap的添加節點(put()方法)
 
 
對於 TreeMap 而言,因爲它底層採用一棵「紅黑樹」來保存集合中的 Entry,這意味這 TreeMap 添加元素、取出元素的性能都比 HashMap 低(紅黑樹和Hash數據結構上的區別):當 TreeMap 添加元素時,須要經過循環找到新增 Entry 的插入位置,所以比較耗性能;當從 TreeMap 中取出元素時,須要經過循環才能找到合適的 Entry,也比較耗性能。但 TreeMap、TreeSet 比 HashMap、HashSet 的優點在於:TreeMap 中的全部 Entry 老是按 key 根據指定排序規則保持有序狀態,TreeSet 中全部元素老是根據指定排序規則保持有序狀態。
爲了很好的理解TreeMap你必須先理解紅黑樹,然而紅黑樹又是一種特殊的二叉查找樹,因此你必須先看兩篇博客
由於我這兩篇博客已經講了不少相關知識,因此這裏就不列出來了。
掌握紅黑樹數據結構的理論以後,咱們來分析TreeMap添加節點(TreeMap 中使用 Entry 內部類表明節點)的實現,TreeMap 集合的 put(K key, V value) 方法實現了將 Entry 放入排序二叉樹中,下面是該方法的源代碼:
[java] view plain copy
 
print?在CODE上查看代碼片派生到個人代碼片
  1. public V put(K key, V value)   
  2. {   
  3.     // 先以 t 保存鏈表的 root 節點  
  4.     Entry<K,V> t = root;   
  5.     // 若是 t==null,代表是一個空鏈表,即該 TreeMap 裏沒有任何 Entry   
  6.     if (t == null)   
  7.     {   
  8.         // 將新的 key-value 建立一個 Entry,並將該 Entry 做爲 root   
  9.         root = new Entry<K,V>(key, value, null);   
  10.         // 設置該 Map 集合的 size 爲 1,表明包含一個 Entry   
  11.         size = 1;   
  12.         // 記錄修改次數爲 1   
  13.         modCount++;   
  14.         return null;   
  15.     }   
  16.     int cmp;   
  17.     Entry<K,V> parent;   
  18.     Comparator<? super K> cpr = comparator;   
  19.     // 若是比較器 cpr 不爲 null,即代表採用定製排序  
  20.     if (cpr != null)   
  21.     {   
  22.         do {   
  23.             // 使用 parent 上次循環後的 t 所引用的 Entry   
  24.             parent = t;   
  25.             // 拿新插入 key 和 t 的 key 進行比較  
  26.             cmp = cpr.compare(key, t.key);   
  27.             // 若是新插入的 key 小於 t 的 key,t 等於 t 的左邊節點  
  28.             if (cmp < 0)   
  29.                 t = t.left;   
  30.             // 若是新插入的 key 大於 t 的 key,t 等於 t 的右邊節點  
  31.             else if (cmp > 0)   
  32.                 t = t.right;   
  33.             // 若是兩個 key 相等,新的 value 覆蓋原有的 value,  
  34.             // 並返回原有的 value   
  35.             else   
  36.                 return t.setValue(value);   
  37.         } while (t != null);   
  38.     }   
  39.     else   
  40.     {   
  41.         if (key == null)   
  42.             throw new NullPointerException();   
  43.         Comparable<? super K> k = (Comparable<? super K>) key;   
  44.         do {   
  45.             // 使用 parent 上次循環後的 t 所引用的 Entry   
  46.             parent = t;   
  47.             // 拿新插入 key 和 t 的 key 進行比較  
  48.             cmp = k.compareTo(t.key);   
  49.             // 若是新插入的 key 小於 t 的 key,t 等於 t 的左邊節點  
  50.             if (cmp < 0)   
  51.                 t = t.left;   
  52.             // 若是新插入的 key 大於 t 的 key,t 等於 t 的右邊節點  
  53.             else if (cmp > 0)   
  54.                 t = t.right;   
  55.             // 若是兩個 key 相等,新的 value 覆蓋原有的 value,  
  56.             // 並返回原有的 value   
  57.             else   
  58.                 return t.setValue(value);   
  59.         } while (t != null);   
  60.     }   
  61.     // 將新插入的節點做爲 parent 節點的子節點  
  62.     Entry<K,V> e = new Entry<K,V>(key, value, parent);   
  63.     // 若是新插入 key 小於 parent 的 key,則 e 做爲 parent 的左子節點  
  64.     if (cmp < 0)   
  65.         parent.left = e;   
  66.     // 若是新插入 key 小於 parent 的 key,則 e 做爲 parent 的右子節點  
  67.     else   
  68.         parent.right = e;   
  69.     // 修復紅黑樹  
  70.     fixAfterInsertion(e);                               // ①  
  71.     size++;   
  72.     modCount++;   
  73.     return null;   
  74. }  

上面這段代碼看起來複雜其實否則,本質上就是紅黑樹德元素插入操做的代碼。看下面紅黑樹插入操做的僞代碼
對比下兩個你會發現其實就是同樣的,因此若是你這裏不理解的話,要先回去看下紅黑樹的知識哦,理解以後,你會發現很簡單
。關於最後的紅黑樹修復操做fixAfterInsertion(e) 我那篇博客中有詳細講,因爲篇幅比較多,就不從新寫了。
 
 
TreeMap的刪除節點
一樣原理仍是紅黑樹節點的刪除,那篇博客也有詳細講解。這裏只給出deleteEntry源碼
 
private void deleteEntry(Entry<K,V> p) 
 { 
    modCount++; 
    size--; 
    // 若是被刪除節點的左子樹、右子樹都不爲空
    if (p.left != null && p.right != null) 
    { 
        // 用 p 節點的中序後繼節點代替 p 節點
        Entry<K,V> s = successor (p); 
        p.key = s.key; 
        p.value = s.value; 
        p = s; 
    } 
    // 若是 p 節點的左節點存在,replacement 表明左節點;不然表明右節點。
    Entry<K,V> replacement = (p.left != null ? p.left : p.right); 
    if (replacement != null) 
    { 
        replacement.parent = p.parent; 
        // 若是 p 沒有父節點,則 replacemment 變成父節點
        if (p.parent == null) 
            root = replacement; 
        // 若是 p 節點是其父節點的左子節點
        else if (p == p.parent.left) 
            p.parent.left  = replacement; 
        // 若是 p 節點是其父節點的右子節點
        else 
            p.parent.right = replacement; 
        p.left = p.right = p.parent = null; 
        // 修復紅黑樹
        if (p.color == BLACK) 
            fixAfterDeletion(replacement);       // ①
    } 
    // 若是 p 節點沒有父節點
    else if (p.parent == null) 
    { 
        root = null; 
    } 
    else 
    { 
        if (p.color == BLACK) 
            // 修復紅黑樹
            fixAfterDeletion(p);                 // ②
        if (p.parent != null) 
        { 
            // 若是 p 是其父節點的左子節點
            if (p == p.parent.left) 
                p.parent.left = null; 
            // 若是 p 是其父節點的右子節點
            else if (p == p.parent.right) 
                p.parent.right = null; 
            p.parent = null; 
        } 
    } 
 }
 

檢索節點數據結構

當 TreeMap 根據 key 來取出 value 時,TreeMap 對應的方法以下:多線程

 public V get(Object key) 
 { 
    // 根據指定 key 取出對應的 Entry 
    Entry>K,V< p = getEntry(key); 
    // 返回該 Entry 所包含的 value 
    return (p==null ? null : p.value); 
 }

從上面程序的粗體字代碼能夠看出,get(Object key) 方法實質是因爲 getEntry() 方法實現的,這個 getEntry() 方法的代碼以下:ide

 final Entry<K,V> getEntry(Object key) 
 { 
    // 若是 comparator 不爲 null,代表程序採用定製排序
    if (comparator != null) 
        // 調用 getEntryUsingComparator 方法來取出對應的 key 
        return getEntryUsingComparator(key); 
    // 若是 key 形參的值爲 null,拋出 NullPointerException 異常
    if (key == null) 
        throw new NullPointerException(); 
    // 將 key 強制類型轉換爲 Comparable 實例
    Comparable<? super K> k = (Comparable<? super K>) key; 
    // 從樹的根節點開始
    Entry<K,V> p = root; 
    while (p != null) 
    { 
        // 拿 key 與當前節點的 key 進行比較
        int cmp = k.compareTo(p.key); 
        // 若是 key 小於當前節點的 key,向「左子樹」搜索
        if (cmp < 0) 
            p = p.left; 
        // 若是 key 大於當前節點的 key,向「右子樹」搜索
        else if (cmp > 0) 
            p = p.right; 
        // 不大於、不小於,就是找到了目標 Entry 
        else 
            return p; 
    } 
    return null; 
 }

上面的 getEntry(Object obj) 方法也是充分利用排序二叉樹的特徵來搜索目標 Entry,程序依然從二叉樹的根節點開始,若是被搜索節點大於當前節點,程序向「右子樹」搜索;若是被搜索節點小於當前節點,程序向「左子樹」搜索;若是相等,那就是找到了指定節點。工具

當 TreeMap 裏的 comparator != null 即代表該 TreeMap 採用了定製排序,在採用定製排序的方式下,TreeMap 採用 getEntryUsingComparator(key) 方法來根據 key 獲取 Entry。下面是該方法的代碼:性能

 final Entry<K,V> getEntryUsingComparator(Object key) 
 { 
    K k = (K) key; 
    // 獲取該 TreeMap 的 comparator 
    Comparator<? super K> cpr = comparator; 
    if (cpr != null) 
    { 
        // 從根節點開始
        Entry<K,V> p = root; 
        while (p != null) 
        { 
            // 拿 key 與當前節點的 key 進行比較
            int cmp = cpr.compare(k, p.key); 
            // 若是 key 小於當前節點的 key,向「左子樹」搜索
            if (cmp < 0) 
                p = p.left; 
            // 若是 key 大於當前節點的 key,向「右子樹」搜索
            else if (cmp > 0) 
                p = p.right; 
            // 不大於、不小於,就是找到了目標 Entry 
            else 
                return p; 
        } 
    } 
    return null; 
 }

其實 getEntry、getEntryUsingComparator 兩個方法的實現思路徹底相似,只是前者對天然排序的 TreeMap 獲取有效,後者對定製排序的 TreeMap 有效。ui

經過上面源代碼的分析不難看出,TreeMap 這個工具類的實現其實很簡單。或者說:從內部結構來看,TreeMap 本質上就是一棵「紅黑樹」,而 TreeMap 的每一個 Entry 就是該紅黑樹的一個節點。spa

3. 常見問題

----------------------------------------------
 
」爲何TreeMap採用紅黑樹而不是二叉查找樹?

其實這個問題就是在問紅黑樹相對於排序二叉樹的優勢。咱們都知道排序二叉樹雖然能夠快速檢索,但在最壞的狀況下:若是插入的節點集自己就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後獲得的排序二叉樹將變成鏈表:全部節點只有左節點(若是插入節點集自己是大到小排列);或全部節點只有右節點(若是插入節點集自己是小到大排列)。在這種狀況下,排序二叉樹就變成了普通鏈表,其檢索效率就會不好。

爲了改變排序二叉樹存在的不足,Rudolf Bayer 與 1972 年發明了另外一種改進後的排序二叉樹:紅黑樹,他將這種排序二叉樹稱爲「對稱二叉 B 樹」,而紅黑樹這個名字則由 Leo J. Guibas 和 Robert Sedgewick 於 1978 年首次提出。

紅黑樹是一個更高效的檢索二叉樹,所以經常用來實現關聯數組。典型地,JDK 提供的集合類 TreeMap 自己就是一個紅黑樹的實現。

紅黑樹在原有的排序二叉樹增長了以下幾個要求:

Java 實現的紅黑樹

上面的性質 3 中指定紅黑樹的每一個葉子節點都是空節點,並且並葉子節點都是黑色。但 Java 實現的紅黑樹將使用 null 來表明空節點,所以遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每一個葉子節點都是紅色的。

  • 性質 1:每一個節點要麼是紅色,要麼是黑色。
  • 性質 2:根節點永遠是黑色的。
  • 性質 3:全部的葉節點都是空節點(即 null),而且是黑色的。
  • 性質 4:每一個紅色節點的兩個子節點都是黑色。(從每一個葉子到根的路徑上不會有兩個連續的紅色節點)
  • 性質 5:從任一節點到其子樹中每一個葉子節點的路徑都包含相同數量的黑色節點。

Java 中實現的紅黑樹可能有如圖 6 所示結構:

圖 6. Java 紅黑樹的示意
圖 6. Java 紅黑樹的示意

備註:本文中全部關於紅黑樹中的示意圖採用白色表明紅色。黑色節點仍是採用了黑色表示。

根據性質 5:紅黑樹從根節點到每一個葉子節點的路徑都包含相同數量的黑色節點,所以從根節點到葉子節點的路徑中包含的黑色節點數被稱爲樹的「黑色高度(black-height)」。

性質 4 則保證了從根節點到葉子節點的最長路徑的長度不會超過任何其餘路徑的兩倍。假若有一棵黑色高度爲 3 的紅黑樹:從根節點到葉節點的最短路徑長度是 2,該路徑上全是黑色節點(黑節點 - 黑節點 - 黑節點)。最長路徑也只可能爲 4,在每一個黑色節點之間插入一個紅色節點(黑節點 - 紅節點 - 黑節點 - 紅節點 - 黑節點),性質 4 保證毫不可能插入更多的紅色節點。因而可知,紅黑樹中最長路徑就是一條紅黑交替的路徑。

紅黑樹和平衡二叉樹

紅黑樹並非真正的平衡二叉樹,但在實際應用中,紅黑樹的統計性能要高於平衡二叉樹,但極端性能略差。

由此咱們能夠得出結論:對於給定的黑色高度爲 N 的紅黑樹,從根到葉子節點的最短路徑長度爲 N-1,最長路徑長度爲 2 * (N-1)。

提示:排序二叉樹的深度直接影響了檢索的性能,正如前面指出,當插入節點自己就是由小到大排列時,排序二叉樹將變成一個鏈表,這種排序二叉樹的檢索性能最低:N 個節點的二叉樹深度就是 N-1。

紅黑樹經過上面這種限制來保證它大體是平衡的——由於紅黑樹的高度不會無限增高,這樣保證紅黑樹在最壞狀況下都是高效的,不會出現普通排序二叉樹的狀況。

因爲紅黑樹只是一個特殊的排序二叉樹,所以對紅黑樹上的只讀操做與普通排序二叉樹上的只讀操做徹底相同,只是紅黑樹保持了大體平衡,所以檢索性能比排序二叉樹要好不少。

但在紅黑樹上進行插入操做和刪除操做會致使樹再也不符合紅黑樹的特徵,所以插入操做和刪除操做都須要進行必定的維護,以保證插入節點、刪除節點後的樹依然是紅黑樹。

 

」TreeMap、TreeSet 對比 HashMap、HashSet的優缺點?「 

 

缺點:

       對於 TreeMap 而言,因爲它底層採用一棵「紅黑樹」來保存集合中的 Entry,這意味這 TreeMap 添加元素、取出元素的性能都比 HashMap (O(1))低:

 

  • 當 TreeMap 添加元素時,須要經過循環找到新增 Entry 的插入位置,所以比較耗性能(O(logN))
  • 當從 TreeMap 中取出元素時,須要經過循環才能找到合適的 Entry,也比較耗性能(O(logN))
優勢:

 

         TreeMap 中的全部 Entry 老是按 key 根據指定排序規則保持有序狀態,TreeSet 中全部元素老是根據指定排序規則保持有序狀態。

相關文章
相關標籤/搜索