1. 程式人生 > >java集合類深入分析之TreeMap/TreeSet篇

java集合類深入分析之TreeMap/TreeSet篇

轉載自:http://shmilyaw-hotmail-com.iteye.com/blog/1836431

簡介

    TreeMap和TreeSet算是java集合類裡面比較有難度的資料結構。和普通的HashMap不一樣,普通的HashMap元素存取的時間複雜度一般是O(1)的範圍。而TreeMap內部對元素的操作複雜度為O(logn)。雖然在元素的存取方面TreeMap並不佔優,但是它內部的元素都是排序的,當需要查詢某些元素以及順序輸出元素的時候它能夠帶來比較理想的結果。可以說,TreeMap是一個內部元素排序版的HashMap。這裡會對TreeMap內部的具體實現機制和它所基於的紅黑樹做一個詳細的介紹。另外,針對具體jdk裡面TreeMap的詳細實現,這裡也會做詳細的分析。

TreeMap和TreeSet之間的關係

    和前面一篇文章類似,這裡比較有意思的地方是,似乎有Map和Set的地方,Set幾乎都成了Map的一個馬甲。此話怎講呢?在前面一篇討論HashMap和HashSet的詳細實現討論裡,我們發現HashSet的詳細實現都是通過封裝了一個HashMap的成員變數來實現的。這裡,TreeSet也不例外。我們先看部分程式碼:

裡面聲明瞭成員變數:

Java程式碼  收藏程式碼
  1. private transient NavigableMap<E,Object> m;  

    這裡NavigableMap本身是TreeMap所實現的一個介面。我們再看下面和建構函式相關的實現:

Java程式碼  收藏程式碼
  1. TreeSet(NavigableMap<E,Object> m) {  
  2.     this.m = m;  
  3. }  
  4. public TreeSet() {   // 無引數建構函式  
  5.     this(new TreeMap<E,Object>());  
  6. }  
  7. public TreeSet(Comparator<? super E> comparator) { // 包含比較器的建構函式  
  8.     this(new TreeMap<>(comparator));  
  9. }  
  10. }  

    這裡建構函式相關部分的程式碼看起來比較多,實際上主要的建構函式就兩個,一個是預設的無引數建構函式和一個比較器建構函式,他們內部的實現都是使用的TreeMap,而其他相關的建構函式都是通過呼叫這兩個來實現的,故其底層使用的就是TreeMap。既然TreeSet只是TreeMap的一個馬甲,我們就只要重點關注一下TreeMap裡面的實現好了。

紅黑樹(Red-Black Tree)

    紅黑樹本質上是一棵一定程度上相對平衡的二叉搜尋樹。為什麼這麼說呢?我們從前面討論二叉搜尋樹的文章中可以看到。一棵二叉搜尋樹理想情況下的搜尋和其他元素操作的時間複雜度是O(logn)。但是,這是基於一個前提,即二叉搜尋樹本身構造出來的樹是平衡的。如果我們按照普通的插入一個元素就按照二叉樹對應關係去擺的話,在一些極端的情況下會失去平衡。比如說我們通過插入一個順序遞增或者遞減的一組元素,那麼最後的結構就相當於一個雙向連結串列。對其中元素的訪問也不可能達到O(logn)這樣的級別。

    所以,在這樣的情況下,我們就希望有那麼一種機制或者資料結構能夠保證我們既能構造出一棵二叉搜尋樹來,而且它天生就是平衡的。這樣就有了紅黑樹。當然,為了同時達到這兩個目標,紅黑樹設定了一些特定的屬性限制,也使得它本身的實現比較複雜。我們在下面的定義中就可以看到。

    紅黑樹的官方定義如下:

紅黑樹是一種二叉樹,同時它還滿足下列5個特性:

1. 每個節點是紅色或者黑色的。

2. 根節點是黑色的。

3. 每個葉節點是黑色的。(這裡將葉節點的左右空子節點作為一個特殊的節點對待,設定他們必須是黑色的。)

4. 如果一個節點是紅色的,則它的左右子節點都必須是黑色的。

5. 對任意一個節點來說,從它到葉節點的所有路徑必須包含相同數目的黑色節點。

    這部分的定義看得讓人有點不知所云,我們先看一個紅黑樹的示例:

    假定其中帶陰影的節點為紅色節點,則上圖為一棵紅黑樹。假定我們取根節點來考察,它到任意一個葉節點要走過3個黑色的節點。這樣,從任意一個節點到葉節點只需要經歷過的黑色節點相同就可以了,可以說這是一個放鬆了的平衡衡量標準。

節點定義

    現在,結合我們前面對平衡二叉搜尋樹的討論和TreeMap裡面要求的特性我們來做一個分析。我們要求設計的TreeMap它的本質上也是一個Map,那麼它意味著對任意一個名值對,我們都需要儲存在資料結構裡面。對於一個名值對來說,key的作用就是用來定址的。在HashMap裡面,key是通過hash函式運算直接對映到對應的slot,這裡則是通過查詢比較放到一棵二叉樹裡一個合適的位置。這個位置則相當於一個slot。所以我們的節點裡面必須有一個key,一個value。

    另外,考慮到這裡將節點定義成了紅色和黑色,所以需要有一個儲存節點顏色的屬性。前面我們討論二叉搜尋樹的時候討論元素的插入和刪除等操作的時候提到過,如果給每個元素增加一個指向父節點的引用,會帶來極大的便利。既然紅黑樹也是其中一種,這種引用肯定就應該考慮了。

    綜上所述,我們的節點應該包含以下6個部分:

1. 左子節點引用

2. 右子節點引用

3. 父節點引用

4. key

5. value

6. color

    這一個結構相當於一個如下的圖:

    在jdk的實現裡,它的定義如下:

Java程式碼  收藏程式碼
  1. static final class Entry<K,V> implements Map.Entry<K,V> {  
  2.     K key;  
  3.     V value;  
  4.     Entry<K,V> left = null;  
  5.     Entry<K,V> right = null;  
  6.     Entry<K,V> parent;  
  7.     boolean color = BLACK;  
  8.     /** 
  9.      * Make a new cell with given key, value, and parent, and with 
  10.      * {@code null} child links, and BLACK color. 
  11.      */  
  12.     Entry(K key, V value, Entry<K,V> parent) {  
  13.         this.key = key;  
  14.         this.value = value;  
  15.         this.parent = parent;  
  16.     }  
  17.     // ... Ignored  
  18. }  

    它是被定義為Entry的內部類。

新增元素

    新增元素的過程可以大致的分為兩個步驟。和前面的二叉搜尋樹類似,我們新增元素也是需要通過比較元素的值,找到新增元素的地方。這部分基本上沒有什麼變化。第二步則是一個調整的過程。因為紅黑樹不一樣,當我們新增一個新的元素之後可能會破壞它固有的屬性。主要在於兩個地方,一個是要保證新加入元素後,到所有葉節點的黑色節點還是一樣的。另外也要保證紅色節點的子節點為黑色節點。

    還有一個就是,結合TreeMap的map特性,我們新增元素的時候也可能會出現新加入的元素key已經在數中間存在了,那麼這個時候就不是新加入元素,而是要更新原有元素的值。

    結合前面提到的這幾個大的思路,我們來看看新增元素的程式碼:

Java程式碼  收藏程式碼
  1. public V put(K key, V value) {  
  2.     Entry<K,V> t = root;  
  3.     if (t == null) {  
  4.         compare(key, key); // type (and possibly null) check  
  5.         root = new Entry<>(key, value, null);  
  6.         size = 1;  
  7.         modCount++;  
  8.         return null;  
  9.     }  
  10.     int cmp;  
  11.     Entry<K,V> parent;  
  12.     // split comparator and comparable paths  
  13.     Comparator<? super K> cpr = comparator;  
  14.     if (cpr != null) {  
  15.         do {  
  16.             parent = t;  
  17.             cmp = cpr.compare(key, t.key);  
  18.             if (cmp < 0)  
  19.                 t = t.left;  
  20.             else if (cmp > 0)  
  21.                 t = t.right;  
  22.             else  
  23.                 return t.setValue(value);  
  24.         } while (t != null);  
  25.     }  
  26.     else {  
  27.         if (key == null)  
  28.             throw new NullPointerException();  
  29.         Comparable<? super K> k = (Comparable<? super K>) key;  
  30.         do {  
  31.             parent = t;  
  32.             cmp = k.compareTo(t.key);  
  33.             if (cmp < 0)  
  34.                 t = t.left;  
  35.             else if (cmp > 0)  
  36.                 t = t.right;  
  37.             else  
  38.                 return t.setValue(value);  
  39.         } while (t != null);  
  40.     }  
  41.     Entry<K,V> e = new Entry<>(key, value, parent);  
  42.     if (cmp < 0)  
  43.         parent.left = e;  
  44.     else  
  45.         parent.right = e;  
  46.     fixAfterInsertion(e);  
  47.     size++;  
  48.     modCount++;  
  49.     return null;  
  50. }  

     上述的程式碼看起來比較多,不過實際上並不複雜。第3到9行主要是判斷在根節點為null的情況下,我們的put方法相當於直接建立一個節點並關聯到根節點。後面的兩個大的if else塊是用來判斷是否設定了comparator的情況下的比較和加入元素操作。對於一些普通的資料型別,他們預設實現了Comparable介面,所以我們用compareTo方法來比較他們。而對於一些自定義實現的類,他們的比較關係在一些特殊情況下需要實現Comparator介面,這就是為什麼前面要針對這兩個部分要進行區分。在這兩個大的塊裡面主要做的就是找到要新增元素的地方,如果有相同key的情況,則直接替換原來的value。

    第42行及後面的部分需要處理新增元素的情況。如果在前面的迴圈塊裡面沒有找到對應的Key值,則說明已經找到了需要插入元素的位置,這裡則要在這個地方加入進去。添加了元素之後,基本上整個過程就結束了。

    這裡有一個方法fixAfterInsertion(),在我們前面的討論中提到過。每次當我們插入一個元素的時候,我們新增的元素會帶有一個顏色,而這個顏色不管是紅色或者黑色都可能會破壞紅黑樹定義的屬性。所以,這裡需要通過一個判斷調整的過程來保證添加了元素後整棵樹還是符合要求的。這部分的過程比較複雜,我們拆開來詳細的一點點講。

     在看fixAfterInsertion的實現之前,我們先看一下樹的左旋和右旋操作。這個東西在fixAfterInsertion裡面用的非常多。

旋轉

    樹的左旋和右旋的過程用一個圖來表示比較簡單直觀:

     從圖中可以看到,我們的左旋和右旋主要是通過交換兩個節點的位置,同時將一個節點的子節點轉變為另外一個節點的子節點。具體以左旋為例,在旋轉前,x是y的父節點。旋轉之後,y成為x的父節點,同時y的左子節點成為x的右子節點。x原來的父節點成為後面y的父節點。這麼一通折騰過程就成為左旋了。同理,我們也可以得到右旋的過程。

     左旋和右旋的實現程式碼如下:

Java程式碼  收藏程式碼
  1. private void rotateLeft(Entry<K,V> p) {  
  2.     if (p != null) {  
  3.         Entry<K,V> r = p.right;  
  4.         p.right = r.left;  
  5.         if (r.left != null)  
  6.             r.left.parent = p;  
  7.         r.parent = p.parent;  
  8.         if (p.parent == null)  
  9.             root = r;  
  10.         else if (p.parent.left == p)  
  11.             p.parent.left = r;  
  12.         else  
  13.             p.parent.right = r;  
  14.         r.left = p;  
  15.         p.parent = r;  
  16.     }  
  17. }  
  18. private void rotateRight(Entry<K,V> p) {  
  19.     if (p != null) {  
  20.         Entry<K,V> l = p.left;  
  21.         p.left = l.right;  
  22.         if (l.right != null) l.right.parent = p;  
  23.         l.parent = p.parent;  
  24.         if (p.parent == null)  
  25.             root = l;  
  26.         else if (p.parent.right == p)  
  27.             p.parent.right = l;  
  28.         else p.parent.left = l;  
  29.         l.right = p;  
  30.         p.parent = l;  
  31.     }  
  32. }  

     這部分的程式碼結合前面的圖來看的話就比較簡單。主要是子節點的移動和判斷父節點並調整。有點像雙向連結串列中間調整元素。

調整過程

    我們知道,在紅黑樹裡面,如果加入一個黑色節點,則導致所有經過這個節點的路徑黑色節點數量增加1,這樣就肯定破壞了紅黑樹中到所有葉節點經過的黑色節點數量一樣的約定。所以,我們最簡單的辦法是先設定加入的節點是紅色的。這樣就不會破壞這一條約定。但是,這樣的調整也會帶來另外一個問題,如果我這個要加入的節點它的父節點已經是紅色的了呢?這豈不是又破壞了原來的約定嗎?是的,在這種情況下,我們就要通過一系列的調整來保證最終它成為一棵合格的紅黑樹。但是這樣比我們加入一個黑色節點然後去調整相對來說範圍要狹窄一些。現在我們來看看怎麼個調整法。

我們假設要新增的節點為N。

場景1: N節點的父節點P以及P的兄弟節點都是紅色,而它的祖父節點G為黑色

   在這種情況下,只要將它的父節點P以及節點U設定為黑色,而祖父節點G設定為紅色。這樣就保證了任何通過G到下面的葉節點經歷的黑色節點還是和原來一樣,為1.而且也保證了紅色節點的子節點不為紅色。這種場景的一個前提是隻要保證要新增的節點和它的父節點以及父節點的兄弟節點都是紅色,則通過同樣的手法進行轉換。這和加入的節點是父節點的左右子節點無關。

場景2: N節點的父節點P是紅色,但是它的祖父節點G和它父節點的兄弟節點U為黑色。

    這種情形實際上還取決於要插入的元素N的位置,如果它是P的右子節點,則先做一個左旋操作,轉換成右邊的情形。這樣,新加入的節點保證成為父節點的左子節點。

    在上圖做了這麼一種轉換之後,我們還需要做下一步的調整,如下圖:

相關推薦

java集合深入分析TreeMap/TreeSet

轉載自:http://shmilyaw-hotmail-com.iteye.com/blog/1836431 簡介     TreeMap和TreeSet算是java集合類裡面比較有難度的資料結構。和普通的HashMap不一樣,普通的HashMap元素

java集合深入分析HashSet, HashMap

    Map和Set是比較常用的兩種資料結構。我們在平常的程式設計中經常會用到他們。只是他們的內部實現機制到底是怎麼樣的呢?瞭解他們的具體實現對於我們如何有效的去使用他們也是很有幫助的。這裡主要是針對Map, Set這兩種型別的資料結構規約和典型的HashMap,HashSet實現做一個討論。 Map

Java集合-ArrayList分析

empty java 如果 all ansi code ati class des ArrayList的特點 可以動態擴容 非線程安全 支持序列化 線程安全的List Collections.synchronizedList、CopyOnWriteArrayList 成員

java集合學習筆記LinkList

prev strong 內部數據 ins 屬性 aced 裏的 row return 1、簡述     LinkList的底層其實就是一個雙向鏈表,所謂的鏈表就是一個LinkList內部靜態靜態類(Node),對LinkList的所有操作本質上就是通過對LinkList中新

java集合學習筆記LinkedHashMap

super 增長 remove sta extend red for normal 順序 1、簡述     LinkedHashMap是HashMap的子類,他們最大的不同是,HashMap內部維護的是一個單向的鏈表數組,而LinkedHashMap內部維護的是一個雙向的鏈

Java集合源碼分析LikedList

sel 屬性 for循環 logs 參考 容量 轉化 簡單 osc 一、LinkedList結構   LinkedList是一種可以在任何位置進行高效地插入和移除操作的有序序列,它是基於雙向鏈表實現的。   LinkedList 是一個繼承於AbstractSequ

java 集合深入理解

2017-08-10 package collection.list; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.ParameterizedTyp

Java Object 深入分析

目錄 1.類構造器public Object(); 2.private static native void registerNatives(); 3.protected native Object clone() throws CloneNotSupportedException;

Java集合原始碼閱讀AbstractCollection

private static <T> T[] finishToArray(T[] r, Iterator<?> it) { int i = r.length; while (it.hasNext()) { int cap = r.

Java集合---(TreeSet排序分析,重複元素判斷,集合輸出)

繼上篇部落格繼續 TreeSet排序分析 重複元素判斷 集合輸出 迭代輸出:Iterator 雙向迭代介面:ListIterator 列舉輸出: Enumeration

java集合源碼分析List(一)

col 實現類 並且 link arraylist oar print 適用於 for   首先分析一下集合與數組的區別:1.java中的數組一般用於存儲基本數據類型,而且是靜態的,即長度固定不變,這就不適用於元素個數未知的情況;2.集合只能用於存儲引用類型,並且長度可變,

java集合源碼分析List(二)

頻繁 null 並且 reel closed tco 默認 java集合 進行 這一節主要介紹List接口的幾個實現類的區別: 1.線程安全 Vector是線程安全的,而ArrayList和LinkedList是非線程安全的。從源碼中我們可知,Vector類中的方法大部分

扒一扒系列開發中常用的Java集合(ArrayList jdk 1.7)

mda des obj 初始設置 onu util private 內部 會有 關於這個系列,因為開發主要用的是java語言,一直想寫寫java開發中常用的一些類(雖然這才是開始的第三篇>_<),所有就起了“扒一扒”系列。這個系列會有框架

編程開發--Java集合繼承與實現必備知識

編程開發 next() int end long dha cos dHash IV 1、LinkedHashSet有序鏈式集合 舉例: long startTime=System.currentTimeMillis(); LinkedHashSet oprTypeSe

java集合ArrayList詳解

int() 相等 toa isempty ont ati urn 影響 輸入 一、ArrayList源碼分析 1、全局變量 (1)默認容量(主要是通過無參構造函數創建ArrayList時第一次add執行擴容操作時指定的elementData的數組容量為10) privat

java集合LinkedList詳解

list詳解 兩種 由於 list接口 add 不為 sel 結點 ESS 一、LinkedList簡介 由於LinkedList是一個實現了Deque的雙端隊列,所以LinkedList既可以當做Queue,又可以當做Stack,在將LinkedList當做Stack時,

java 集合ArrayList

本文將從原始碼的角度對Java 最常用的集合類ArrayList進行介紹,程式碼版本為1.8_121。 繼承結構 除了一些功能性的介面,ArrayList的繼承大致可以看成是從Collection=>AbstractCollection=>AbstractList=&g

Java集合介面學習

一、前言       在Java中使用介面能規範實現該介面的類該實現的功能,介紹Java集合類的介面有助於對Java集合整體、對不同場景該使用什麼樣的集合有個明確的認識,對於學習Java開發的人來說,Java標準庫集合的學習是必經之路,所以自今天起,我打算每天從

Java集合】LinkedList原始碼分析(jdk1.8)

ArrayList和LinkedList是List介面的兩種實現,具有相同的查詢、插入、刪除操作,只是底層的實現方式不一樣。LinkedList是以雙向連結串列形式實現的集合類。 其增刪操作由於不需要移

java集合,List和Set比較,各自的子比較(ArrayList,Vector,LinkedList;HashSet,TreeSet),Map集合比較

ArrayList,LinkedList,Vector都屬於ListList:元素是有順序的,元素可以重複因為每個元素有自己的角標(索引)|-- ArrayList:底層是陣列結構,特點是:查詢很快,增刪稍微慢點,執行緒不同步:A執行緒將元素放在索引0位置,CPU排程執行緒A停止,B執行,也將元素放在索引0位