原文地址http://www.cnblogs.com/xrq730/p/6867924.html,轉載請註明出處,謝謝!html
初識TreeMap算法
以前的文章講解了兩種Map,分別是HashMap與LinkedHashMap,它們保證了以O(1)的時間複雜度進行增、刪、改、查,從存儲角度考慮,這兩種數據結構是很是優秀的。另外,LinkedHashMap還額外地保證了Map的遍歷順序能夠與put順序一致,解決了HashMap自己無序的問題。安全
儘管如此,HashMap與LinkedHashMap仍是有本身的侷限性----它們不具有統計性能,或者說它們的統計性能時間複雜度並非很好才更準確,全部的統計必須遍歷全部Entry,所以時間複雜度爲O(N)。好比Map的Key有一、二、三、四、五、六、7,我如今要統計:數據結構
就相似這些操做,HashMap和LinkedHashMap作得比較差,此時咱們可使用TreeMap。TreeMap的Key按照天然順序進行排序或者根據建立映射時提供的Comparator接口進行排序。TreeMap爲增、刪、改、查這些操做提供了log(N)的時間開銷,從存儲角度而言,這比HashMap與LinkedHashMap的O(1)時間複雜度要差些;可是在統計性能上,TreeMap一樣能夠保證log(N)的時間開銷,這又比HashMap與LinkedHashMap的O(N)時間複雜度好很多。性能
所以總結而言:若是隻須要存儲功能,使用HashMap與LinkedHashMap是一種更好的選擇;若是還須要保證統計性能或者須要對Key按照必定規則進行排序,那麼使用TreeMap是一種更好的選擇。學習
紅黑樹的一些基本概念測試
在講TreeMap前仍是先說一下紅黑樹的一些基本概念,這樣能夠更好地理解以後TreeMap的源代碼。spa
二叉查找樹是在生成的時候是很是容易失衡的,形成的最壞狀況就是一邊倒(即只有左子樹/右子樹),這樣會致使樹檢索的效率大大下降。(關於樹和二叉查找樹能夠看我以前寫的一篇文章樹型結構)線程
紅黑樹是爲了維護二叉查找樹的平衡而產生的一種樹,根據維基百科的定義,紅黑樹有五個特性,但我以爲講得不太易懂,我本身總結一下,紅黑樹的特性大體有三個(換句話說,插入、刪除節點後整個紅黑樹也必須知足下面的三個性質,若是不知足則必須進行旋轉):翻譯
上述的性質約束了紅黑樹的關鍵:從根到葉子的最長可能路徑很少於最短可能路徑的兩倍長。獲得這個結論的理由是:
此時(2)正好是(1)的兩倍長。結果就是這個樹大體上是平衡的,由於好比插入、刪除和查找某個值這樣的操做最壞狀況都要求與樹的高度成比例,這個高度的理論上限容許紅黑樹在最壞狀況下都是高效的,而不一樣於普通的二叉查找樹,最終保證了紅黑樹可以以O(log2 n) 的時間複雜度進行搜索、插入、刪除。
下面展現一張紅黑樹的實例圖:
能夠看到根節點到全部NULL LEAF節點(即葉子節點)所通過的黑色節點都是2個。
另外從這張圖上咱們還能獲得一個結論:紅黑樹並非高度的平衡樹。所謂平衡樹指的是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,可是咱們看:
左右子樹的高度差值爲2,所以紅黑樹並非高度平衡的,它放棄了高度平衡的特性而只追求部分平衡,這種特性下降了插入、刪除時對樹旋轉的要求,從而提高了樹的總體性能。而其餘平衡樹好比AVL樹雖然查找性能爲性能是O(logn),可是爲了維護其平衡特性,可能要在插入、刪除操做時進行屢次的旋轉,產生比較大的消耗。
四個關注點在TreeMap上的答案
關 注 點 | 結 論 |
TreeMap是否容許鍵值對爲空 | Key不容許爲空,Value容許爲空 |
TreeMap是否容許重複數據 | Key重複會覆蓋,Value容許重複 |
TreeMap是否有序 | 按照Key的天然順序排序或者Comparator接口指定的排序算法進行排序 |
TreeMap是否線程安全 | 非線程安全 |
TreeMap基本數據結構
TreeMap基於紅黑樹實現,既然是紅黑樹,那麼每一個節點中除了Key-->Value映射以外,必然存儲了紅黑樹節點特有的一些內容,它們是:
TreeMap的節點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 }
因爲顏色只有紅色和黑色兩種,所以顏色可使用布爾類型(boolean)來表示,黑色表示爲true,紅色爲false。
TreeMap添加數據流程總結
首先看一下TreeMap如何添加數據,測試代碼爲:
1 public class MapTest { 2 3 @Test 4 public void testTreeMap() { 5 TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>(); 6 treeMap.put(10, "10"); 7 treeMap.put(85, "85"); 8 treeMap.put(15, "15"); 9 treeMap.put(70, "70"); 10 treeMap.put(20, "20"); 11 treeMap.put(60, "60"); 12 treeMap.put(30, "30"); 13 treeMap.put(50, "50"); 14 15 for (Map.Entry<Integer, String> entry : treeMap.entrySet()) { 16 System.out.println(entry.getKey() + ":" + entry.getValue()); 17 } 18 } 19 20 }
本文接下來的內容會給出插入每條數據以後紅黑樹的數據結構是什麼樣子的。首先看一下treeMap的put方法的代碼實現:
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 6 root = new Entry<>(key, value, null); 7 size = 1; 8 modCount++; 9 return null; 10 } 11 int cmp; 12 Entry<K,V> parent; 13 // split comparator and comparable paths 14 Comparator<? super K> cpr = comparator; 15 if (cpr != null) { 16 do { 17 parent = t; 18 cmp = cpr.compare(key, t.key); 19 if (cmp < 0) 20 t = t.left; 21 else if (cmp > 0) 22 t = t.right; 23 else 24 return t.setValue(value); 25 } while (t != null); 26 } 27 else { 28 if (key == null) 29 throw new NullPointerException(); 30 Comparable<? super K> k = (Comparable<? super K>) key; 31 do { 32 parent = t; 33 cmp = k.compareTo(t.key); 34 if (cmp < 0) 35 t = t.left; 36 else if (cmp > 0) 37 t = t.right; 38 else 39 return t.setValue(value); 40 } while (t != null); 41 } 42 Entry<K,V> e = new Entry<>(key, value, parent); 43 if (cmp < 0) 44 parent.left = e; 45 else 46 parent.right = e; 47 fixAfterInsertion(e); 48 size++; 49 modCount++; 50 return null; 51 }
從這段代碼,先總結一下TreeMap添加數據的幾個步驟:
第1~第5步都沒有什麼問題,紅黑樹最核心的應當是第6步插入數據以後進行的修復工做,對應的Java代碼是TreeMap中的fixAfterInsertion方法,下面看一下put每一個數據以後TreeMap都作了什麼操做,藉此來理清TreeMap的實現原理。
put(10, "10")
首先是put(10, "10"),因爲此時TreeMap中沒有任何節點,所以10爲根且根節點爲黑色節點,put(10, "10")以後的數據結構爲:
put(85, "85")
接着是put(85, "85"),這一步也不難,85比10大,所以在10的右節點上,可是因爲85不是根節點,所以會執行fixAfterInsertion方法進行數據修正,看一下fixAfterInsertion方法代碼實現:
1 private void fixAfterInsertion(Entry<K,V> x) { 2 x.color = RED; 3 4 while (x != null && x != root && x.parent.color == RED) { 5 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { 6 Entry<K,V> y = rightOf(parentOf(parentOf(x))); 7 if (colorOf(y) == RED) { 8 setColor(parentOf(x), BLACK); 9 setColor(y, BLACK); 10 setColor(parentOf(parentOf(x)), RED); 11 x = parentOf(parentOf(x)); 12 } else { 13 if (x == rightOf(parentOf(x))) { 14 x = parentOf(x); 15 rotateLeft(x); 16 } 17 setColor(parentOf(x), BLACK); 18 setColor(parentOf(parentOf(x)), RED); 19 rotateRight(parentOf(parentOf(x))); 20 } 21 } else { 22 Entry<K,V> y = leftOf(parentOf(parentOf(x))); 23 if (colorOf(y) == RED) { 24 setColor(parentOf(x), BLACK); 25 setColor(y, BLACK); 26 setColor(parentOf(parentOf(x)), RED); 27 x = parentOf(parentOf(x)); 28 } else { 29 if (x == leftOf(parentOf(x))) { 30 x = parentOf(x); 31 rotateRight(x); 32 } 33 setColor(parentOf(x), BLACK); 34 setColor(parentOf(parentOf(x)), RED); 35 rotateLeft(parentOf(parentOf(x))); 36 } 37 } 38 } 39 root.color = BLACK; 40 }
咱們看第2行的代碼,它將默認的插入的那個節點着色成爲紅色,這很好理解:
根據紅黑樹的性質(3),紅黑樹要求從根節點到葉子全部葉子節點上通過的黑色節點個數是相同的,所以若是插入的節點着色爲黑色,那必然有可能致使某條路徑上的黑色節點數量大於其餘路徑上的黑色節點數量,所以默認插入的節點必須是紅色的,以此來維持紅黑樹的性質(3)
固然插入節點着色爲紅色節點後,有可能致使的問題是違反性質(2),即出現連續兩個紅色節點,這就須要經過旋轉操做去改變樹的結構,解決這個問題。
接着看第4行的判斷,前兩個條件都知足,可是由於85這個節點的父節點是根節點的,根節點是黑色節點,所以這個條件不知足,while循環不進去,直接執行一次30行的代碼給根節點着色爲黑色(由於在旋轉過程當中有可能致使根節點爲紅色,而紅黑樹的根節點必須是黑色,所以最後無論根節點是否是黑色,都要從新着色確保根節點是黑色的)。
那麼put(85, "85")以後,整個樹的結構變爲:
fixAfterInsertion方法流程
在看put(15, "15")以前,必需要先過一下fixAfterInsertion方法。第5行~第21行的代碼和第21行~第38行的代碼是同樣的,無非一個是操做左子樹另外一個是操做右子樹而已,所以就看前一半:
1 while (x != null && x != root && x.parent.color == RED) { 2 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { 3 Entry<K,V> y = rightOf(parentOf(parentOf(x))); 4 if (colorOf(y) == RED) { 5 setColor(parentOf(x), BLACK); 6 setColor(y, BLACK); 7 setColor(parentOf(parentOf(x)), RED); 8 x = parentOf(parentOf(x)); 9 } else { 10 if (x == rightOf(parentOf(x))) { 11 x = parentOf(x); 12 rotateLeft(x); 13 } 14 setColor(parentOf(x), BLACK); 15 setColor(parentOf(parentOf(x)), RED); 16 rotateRight(parentOf(parentOf(x))); 17 } 18 } 19 .... 20 }
第2行的判斷注意一下,用語言描述出來就是:判斷當前節點的父節點與當前節點的父節點的父節點的左子節點是否同一個節點。翻譯一下就是:當前節點是否左子節點插入,關於這個不明白的我就不解釋了,能夠本身多思考一下。對這整段代碼我用流程圖描述一下:
這裏有一個左子樹內側插入與左子樹點外側插入的概念,我用圖表示一下:
其中左邊的是左子樹外側插入,右邊的是左子樹內側插入,能夠從上面的流程圖上看到,對於這兩種插入方式的處理是不一樣的,區別是後者也就是左子樹內側插入多一步左旋操做。
能看出,紅黑樹的插入最多隻須要進行兩次旋轉,至於紅黑樹的旋轉,後面結合代碼進行講解。
put(15, "15")
看完fixAfterInsertion方法流程以後,繼續添加數據,此次添加的是put(15, "15"),15比10大且比85小,所以15最終應當是85的左子節點,默認插入的是紅色節點,所以首先將15做爲紅色節點插入85的左子節點後的結構應當是:
可是顯然這裏違反了紅黑樹的性質(2),即連續出現了兩個紅色節點,所以此時必須進行旋轉。回看前面fixAfterInsertion的流程,上面演示的是左子樹插入流程,右子樹同樣,能夠看到這是右子樹內側插入,須要進行兩次旋轉操做:
旋轉是紅黑樹中最難理解也是最核心的操做,右旋和左旋是對稱的操做,我我的的理解,以右旋爲例,對某個節點x進行右旋,其實質是:
左旋是一樣的道理,在旋轉的時候必定要記住這兩句話,這將會幫助咱們清楚地知道在不一樣的場景下旋轉如何進行。
先看一下(1)也就是"對新插入節點的父節點進行一次右旋操做",源代碼爲rotateRight方法:
1 private void rotateRight(Entry<K,V> p) { 2 if (p != null) { 3 Entry<K,V> l = p.left; 4 p.left = l.right; 5 if (l.right != null) l.right.parent = p; 6 l.parent = p.parent; 7 if (p.parent == null) 8 root = l; 9 else if (p.parent.right == p) 10 p.parent.right = l; 11 else p.parent.left = l; 12 l.right = p; 13 p.parent = l; 14 } 15 }
右旋流程用流程圖畫一下其流程:
再用一張示例圖表示一下右旋各節點的變化,旋轉不會改變節點顏色,這裏就不區分成色節點和黑色節點了,a是須要進行右旋的節點:
左旋與右旋是一個對稱的操做,你們能夠試試看把右圖的b節點進行左旋,就變成了左圖了。這裏多說一句,旋轉必定要說明是對哪一個節點進行旋轉,網上看不少文章講左旋、右旋都是直接說旋轉以後怎麼樣怎麼樣,我認爲脫離具體的節點講旋轉是沒有任何意義的。
這裏可能會有的一個問題是:b有左右兩個子節點分別爲d和e,爲何右旋的時候要將右子節點e拿到a的左子節點而不是b的左子節點d?
一個很簡單的解釋是:若是將b的左子節點d拿到a的左子節點,那麼b右旋後右子節點指向a,b原來的右子節點e就成爲了一個遊離的節點,遊離於整個數據結構以外。
回到實際的例子,對85這個節點進行右旋以後還有一次着色操做(2),分別是將x的父節點着色爲黑色,將x的祖父節點着色爲紅色,那麼此時的樹形結構應當爲:
而後對節點10進行一次左旋操做(3),左旋以後的結構爲:
最後無論根節點是否是黑色,都將根節點着色爲黑色,那麼插入15以後的數據結構就變爲了上圖,知足紅黑樹的三條特性。
put(70, "70")
put(70, "70")就很簡單了,70是85的左子節點,因爲70的父節點以及叔父節點都是紅色節點,所以直接將70的父節點8五、將70的叔父節點10着色爲黑色便可,70這個節點着色爲紅色,即知足紅黑樹的特性,插入70以後的結構圖爲:
put(20, "20")
put(20, "20"),插入的位置應當是70的左子節點,默認插入紅色,插入以後的結構圖爲:
問題很明顯,出現了連續兩個紅色節點,20的插入位置是一種左子樹外側插入的場景,所以只須要進行着色+對節點85進行一次右旋便可,着色+右旋以後數據結構變爲:
put(60, "60")
下面進行put(60, "60")操做,節點60插入的位置是節點20的右子節點,因爲節點60的父節點與叔父節點都是紅色節點,所以只須要將節點60的父節點與叔父節點着色爲黑色,將節點60的組父節點着色爲紅色便可。
那麼put(60, "60")以後的結構爲:
put(30, "30")
put(30, "30"),節點30應當爲節點60的左子節點,所以插入節點30以後應該是這樣的:
顯然這裏違反了紅黑樹性質(2)即連續出現了兩個紅色節點,所以這裏要進行旋轉。
put(30, "30")的操做和put(15, "15")的操做相似,一樣是右子樹內側插入的場景,那麼須要進行兩次旋轉:
右旋+着色+左旋以後,put(30, "30")的結果應當爲:
put(50, "50")
下一個操做是put(50, "50"),節點50是節點60的左子節點,因爲節點50的父親節點與叔父節點都是紅色節點,所以只須要將節點50的父親節點與叔父節點着色爲黑色,將節點50的祖父節點着色爲紅色便可:
節點50的父節點與叔父節點都是紅色節點(注意不要被上圖迷糊了!上圖是從新着色以後的結構而不是從新着色以前的結構,從新着色以前的結構爲上上圖),所以插入節點50只須要進行着色,自己這樣的操做是沒有任何問題的,但問題的關鍵在於,着色以後出現了連續的紅色節點,即節點30與節點70。這就是爲何fixAfterInsertion方法的方法體是while循環的緣由:
1 private void fixAfterInsertion(Entry<K,V> x) { 2 x.color = RED; 3 4 while (x != null && x != root && x.parent.color == RED) { 5 ... 6 } 7 }
由於這種着色方式是將插入節點的祖父節點着色爲紅色,所以着色以後必須將當前節點指向插入節點的祖父節點,判斷祖父節點與父節點是否連續紅色的節點,是就進行旋轉,從新讓紅黑樹平衡。
接下來的問題就是怎麼旋轉了。咱們能夠把節點15-->節點70-->節點30連起來看,是否是很熟悉?這就是上面重複了兩次的右子樹內側插入的場景,那麼首先對節點70進行右旋,右旋後的結果爲:
下一步,節點70的父節點着色爲黑色,節點70的祖父節點着色爲紅色(這一步不理解或者忘了爲何的,能夠去看一下以前對於fixAfterInsertion方法的解讀),從新着色後的結構爲:
最後一步,對節點70的父節點節點15進行一次左旋,左旋以後的結構爲:
從新恢復紅黑樹的性質:
後記
本文經過不斷向紅黑樹的右子樹插入數據,演示了紅黑樹右側插入時可能出現的各類狀況且應當如何處理這些狀況,左側插入同理。
紅黑樹仍是有點難,所以我我的建議在學習紅黑樹的時候必定要多畫(像我我的就畫了3張A4紙)+多想,這樣才能更好地理解紅黑樹的原理,尤爲是旋轉的原理。
TreeMap的插入操做和旋轉操做已經講完,後文會着眼於TreeMap的刪除操做以及一些統計操做(好比找到節點比50大的全部節點)是如何實現的。