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.   
  5. public TreeSet() {   // 無參數構造函數  
  6.     this(new TreeMap<E,Object>());  
  7. }  
  8.   
  9. public TreeSet(Comparator<? super E> comparator) { // 包含比較器的構造函數  
  10.     this(new TreeMap<>(comparator));  
  11. }  
  12.   
  13. }  

    這裏構造函數相關部分的代碼看起來比較多,實際上主要的構造函數就兩個,一個是默認的無參數構造函數和一個比較器構造函數,他們內部的實現都是使用的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.     /** 
  10.      * Make a new cell with given key, value, and parent, and with 
  11.      * {@code null} child links, and BLACK color. 
  12.      */  
  13.     Entry(K key, V value, Entry<K,V> parent) {  
  14.         this.key = key;  
  15.         this.value = value;  
  16.         this.parent = parent;  
  17.     }  
  18.       
  19.     // ... Ignored  
  20. }  

    它是被定義爲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.   
  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. }  

     上述的代碼看起來比較多,不過實際上並不複雜。第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.   
  19. private void rotateRight(Entry<K,V> p) {  
  20.     if (p != null) {  
  21.         Entry<K,V> l = p.left;  
  22.         p.left = l.right;  
  23.         if (l.right != null) l.right.parent = p;  
  24.         l.parent = p.parent;  
  25.         if (p.parent == null)  
  26.             root = l;  
  27.         else if (p.parent.right == p)  
  28.             p.parent.right = l;  
  29.         else p.parent.left = l;  
  30.         l.right = p;  
  31.         p.parent = l;  
  32.     }  
  33. }  

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

調整過程

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

我們假設要添加的節點爲N。

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

 

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

 

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

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

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

    這一步是通過將P和G的這一段右旋,這樣G則成爲了P的右子節點。然後再將P的顏色變成黑色,G的顏色變成紅色。這樣就保證新的這一部分子樹還是包含相同的黑色子節點。

    前面我們對這兩種情況的討論主要涵蓋了這麼一種大情況,就是假設我們新加入節點N,它的父節點P是祖父節點G的左子節點。在這麼一個大前提下,我們再來想想前面的這幾種場景是否已經足夠完備。我們知道,這裏需要調整的情況必然是新加入的節點N和父節點P出現相同顏色也就是紅色的情況。那麼,在他們同時是紅色而且父節點P是祖父節點G的左子節點的情況下,P的兄弟節點只有兩種可能,要麼爲紅色,要麼爲黑色。這兩種情況正好就是我們前面討論的圖所涵蓋的。

   如果父節點P作爲祖父節點G的右子節點,則情況和作爲左子節點的情況對稱。我們可以按照類似的方法來處理。

 

Java代碼  收藏代碼
  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. }  

    前面代碼中while循環的條件則是判斷當前節點是否有父節點,而且父節點的顏色和它是否同樣爲紅色。我們默認加入的元素都設置成紅色。我在代碼裏把父節點是祖父節點左孩子的情況做了註釋。另外一種情況也可以依葫蘆畫瓢的來分析。

刪除元素 

    刪除元素的過程和普通二叉搜索樹的搜索過程大體也比較類似,首先是根據待刪除節點的情況進行分析:

1. 待刪除節點沒有子節點, 則直接刪除該節點。如下圖:

 2. 待刪除節點有一個子節點,則用該子節點替換它的父節點:

 

3. 待刪除節點有兩個子節點,則取它的後繼節點替換它,並刪除這個後繼節點原來的位置。它可能有兩種情況:

 

 

 

    這幾種情況就是二叉搜索樹裏面刪除元素的過程。這裏就不再贅述。我們主要看紅黑樹有些不一樣的地方。下面是刪除方法實現的主要代碼: 

 

Java代碼  收藏代碼
  1. private void deleteEntry(Entry<K,V> p) {  
  2.     modCount++;  
  3.     size--;  
  4.   
  5.     // If strictly internal, copy successor's element to p and then make p  
  6.     // point to successor.  
  7.     if (p.left != null && p.right != null) {  
  8.         Entry<K,V> s = successor(p);  
  9.         p.key = s.key;  
  10.         p.value = s.value;  
  11.         p = s;  
  12.     } // p has 2 children  
  13.   
  14.     // Start fixup at replacement node, if it exists.  
  15.     Entry<K,V> replacement = (p.left != null ? p.left : p.right);  
  16.   
  17.     if (replacement != null) {  
  18.         // Link replacement to parent  
  19.         replacement.parent = p.parent;  
  20.         if (p.parent == null)  
  21.             root = replacement;  
  22.         else if (p == p.parent.left)  
  23.             p.parent.left  = replacement;  
  24.         else  
  25.             p.parent.right = replacement;  
  26.   
  27.         // Null out links so they are OK to use by fixAfterDeletion.  
  28.         p.left = p.right = p.parent = null;  
  29.   
  30.         // Fix replacement  
  31.         if (p.color == BLACK)  
  32.             fixAfterDeletion(replacement);  
  33.     } else if (p.parent == null) { // return if we are the only node.  
  34.         root = null;  
  35.     } else { //  No children. Use self as phantom replacement and unlink.  
  36.         if (p.color == BLACK)  
  37.             fixAfterDeletion(p);  
  38.   
  39.         if (p.parent != null) {  
  40.             if (p == p.parent.left)  
  41.                 p.parent.left = null;  
  42.             else if (p == p.parent.right)  
  43.                 p.parent.right = null;  
  44.             p.parent = null;  
  45.         }  
  46.     }  
  47. }  

    第7到12行代碼就是判斷和處理待刪除節點如果有兩個子節點的情況。通過找到它的後繼節點,然後將後繼節點的值覆蓋當前節點。這一步驟完成之後,後續的就主要是將原來那個後繼節點刪除。第15行及以後的代碼主要就是處理刪除這個節點的事情。當然,考慮到紅黑樹的特性,這裏有兩個判斷當前待刪除節點是否爲黑色的地方。我們知道,如果當前待刪除節點是紅色的,它被刪除之後對當前樹的特性不會造成任何破壞影響。而如果被刪除的節點是黑色的,這就需要進行進一步的調整來保證後續的樹結構滿足要求。這也就是爲什麼裏面需要調用fixAfterDeletion這個方法。

刪除後的調整

    刪除元素之後的調整和前面的插入元素調整的過程比起來更復雜。它不是一個簡單的在原來過程中取反。我們先從一個最基本的點開始入手。首先一個,我們要進行調整的這個點肯定是因爲我們要刪除的這個點破壞了紅黑樹的本質特性。而如果我們刪除的這個點是紅色的,則它肯定不會破壞裏面的屬性。因爲從前面刪除的過程來看,我們這個要刪除的點是已經在瀕臨葉節點的附近了,它要麼有一個子節點,要麼就是一個葉節點。如果它是紅色的,刪除了,從上面的節點到葉節點所經歷的黑色節點沒有變化。所以,這裏的一個前置條件就是待刪除的節點是黑色的。

    在前面的那個前提下,我們要調整紅黑樹的目的就是要保證,這個原來是黑色的節點被刪除後,我們要通過一定的變化,使得他們仍然是合法的紅黑樹。我們都知道,在一個黑色節點被刪除後,從上面的節點到它所在的葉節點路徑所經歷的黑色節點就少了一個。我們需要做一些調整,使得它少的這個在後面某個地方能夠補上。

    ok,有了這一部分的理解,我們再來看調整節點的幾種情況。 

1. 當前節點和它的父節點是黑色的,而它的兄弟節點是紅色的:

    這種情況下既然它的兄弟節點是紅色的,從紅黑樹的屬性來看,它的兄弟節點必然有兩個黑色的子節點。這裏就通過節點x的父節點左旋,然後父節點B顏色變成紅色,而原來的兄弟節點D變成黑色。這樣我們就將樹轉變成第二種情形中的某一種情況。在做後續變化前,這棵樹這麼的變化還是保持着原來的平衡。

 

2. 1) 當前節點的父節點爲紅色,而它的兄弟節點,包括兄弟節點的所有子節點都是黑色。

 

     在這種情況下,我們將它的兄弟節點設置爲紅色,然後x節點指向它的父節點。這裏有個比較難以理解的地方,就是爲什麼我這麼一變之後它就平衡了呢?因爲我們假定A節點是要調整的節點一路調整過來的。因爲原來那個要調整的節點爲黑色,它一旦被刪除就路徑上的黑色節點少了1.所以這裏A所在的路徑都是黑色節點少1.這裏將A的兄弟節點變成紅色後,從它的父節點到下面的所有路徑就都統一少了1.保證最後又都平衡了。

    當然,大家還會有一個擔憂,就是當前調整的畢竟只是一棵樹中間的字數,這裏頭的節點B可能還有父節點,這麼一直往上到根節點。你這麼一棵字數少了一個黑色節點,要保證整理合格還是不夠的。這裏在代碼裏有了一個保證。假設這裏B已經是紅色的了。那麼代碼裏那個循環塊就跳出來了,最後的部分還是會對B節點,也就是x所指向的這個節點置成黑色。這樣保證前面虧的那一個黑色節點就補回來了。

 2) 當前節點的父節點爲黑色,而它的兄弟節點,包括兄弟節點的所有子節點都是黑色。

    這種情況和前面比較類似。如果接着前面的討論來,在做了那個將兄弟節點置成紅色的操作之後,從父節點B開始的所有子節點都少了1.那麼這裏從代碼中間看的話,由於x指向了父節點,仍然是黑色。則這個時候以父節點B作爲基準的子樹下面都少了黑節點1. 我們就接着以這麼一種情況向上面推進。

 

3.  當前節點的父節點爲紅色,而它的兄弟節點是黑色,同時兄弟節點有一個節點是紅色。

 

 

     這裏所做的操作就是先將兄弟節點做一個右旋操作,轉變成第4種情況。當然,前面的前提是B爲紅色,在B爲黑色的情況下也可以同樣的處理。

 

4. 在當前兄弟節點的右子節點是紅色的情況下。

     這裏是一種比較理想的處理情況,我們將父節點做一個左旋操作,同時將父節點B變成黑色,而將原來的兄弟節點D變成紅色,並將D的右子節點變成黑色。這樣保證了新的子樹中間根節點到各葉子節點的路徑依然是平衡的。大家看到這裏也許會覺得有點奇怪,爲什麼這一步調整結束後就直接x = T.root了呢?也就是說我們一走完這個就可以把x直接跳到根節點,其他的都不需要看了。這是因爲我們前面的一個前提,A節點向上所在的路徑都是黑色節點少了一個的,這裏我們以調整之後相當於給它增加了一個黑色節點,同時對其他子樹的節點沒有任何變化。相當於我內部已經給它補償上來了。所以後續就不需要再往上去調整。

    前面討論的這4種情況是在當前節點是父節點的左子節點的條件下進行的。如果當前節點是父節點的右子節點,則可以對應的做對稱的操作處理,過程也是一樣的。

    具體調整的代碼如下:

Java代碼  收藏代碼
  1. private void fixAfterDeletion(Entry<K,V> x) {  
  2.     while (x != root && colorOf(x) == BLACK) {  
  3.         if (x == leftOf(parentOf(x))) {  
  4.             Entry<K,V> sib = rightOf(parentOf(x));  
  5.   
  6.             if (colorOf(sib) == RED) {  
  7.                 setColor(sib, BLACK);  
  8.                 setColor(parentOf(x), RED);  
  9.                 rotateLeft(parentOf(x));  
  10.                 sib = rightOf(parentOf(x));  
  11.             }  
  12.   
  13.             if (colorOf(leftOf(sib))  == BLACK &&  
  14.                 colorOf(rightOf(sib)) == BLACK) {  
  15.                 setColor(sib, RED);  
  16.                 x = parentOf(x);  
  17.             } else {  
  18.                 if (colorOf(rightOf(sib)) == BLACK) {  
  19.                     setColor(leftOf(sib), BLACK);  
  20.                     setColor(sib, RED);  
  21.                     rotateRight(sib);  
  22.                     sib = rightOf(parentOf(x));  
  23.                 }  
  24.                 setColor(sib, colorOf(parentOf(x)));  
  25.                 setColor(parentOf(x), BLACK);  
  26.                 setColor(rightOf(sib), BLACK);  
  27.                 rotateLeft(parentOf(x));  
  28.                 x = root;  
  29.             }  
  30.         } else { // symmetric  
  31.             Entry<K,V> sib = leftOf(parentOf(x));  
  32.   
  33.             if (colorOf(sib) == RED) {  
  34.                 setColor(sib, BLACK);  
  35.                 setColor(parentOf(x), RED);  
  36.                 rotateRight(parentOf(x));  
  37.                 sib = leftOf(parentOf(x));  
  38.             }  
  39.   
  40.             if (colorOf(rightOf(sib)) == BLACK &&  
  41.                 colorOf(leftOf(sib)) == BLACK) {  
  42.                 setColor(sib, RED);  
  43.                 x = parentOf(x);  
  44.             } else {  
  45.                 if (colorOf(leftOf(sib)) == BLACK) {  
  46.                     setColor(rightOf(sib), BLACK);  
  47.                     setColor(sib, RED);  
  48.                     rotateLeft(sib);  
  49.                     sib = leftOf(parentOf(x));  
  50.                 }  
  51.                 setColor(sib, colorOf(parentOf(x)));  
  52.                 setColor(parentOf(x), BLACK);  
  53.                 setColor(leftOf(sib), BLACK);  
  54.                 rotateRight(parentOf(x));  
  55.                 x = root;  
  56.             }  
  57.         }  
  58.     }  
  59.   
  60.     setColor(x, BLACK);  
  61. }  

 

其他

    TreeMap的紅黑樹實現當然也包含其他部分的代碼實現,如用於查找元素的getEntry方法,取第一個和最後一個元素的getFirstEntry, getLastEntry方法以及求前驅和後繼的predecesor, successor方法。這些方法的實現和普通二叉搜索樹的實現沒什麼明顯差別。這裏就忽略不討論了。這裏還有一個有意思的方法實現,就是buildFromSorted方法。它的實現過程並不複雜,不過經常被作爲面試的問題來討論。後續文章也會針對這個小問題進行進一步的討論。

總結

    在一篇文章裏光要把紅黑樹的來龍去脈折騰清楚就挺麻煩的,如果還要針對它的一個具體jdk的實現代碼進行分析的話,這個話題就顯得比較大了。不過一開始就結合優秀的實現代碼來學習這個數據結構的話,對於自己體會其中的思想和鍛鍊編程的功力還是很有幫助的。TreeMap裏面實現得最出彩的地方還是紅黑樹的部分,當然,還有其他一兩個比較有意思的方法,其問題還經常被作爲一些面試的問題來討論,後續的文章也會針對這部分進行一些分析。

參考資料

http://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html

Introduction to algorithms


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章