尊重作者的成果,本文大部分內容轉自:http://cmsblogs.com/?p=1013(原文);為方便學習注釋使用而)
本節參考文獻:http://baike.baidu.com/view/133754.htm?fr=aladdin—–百度百科 注:由于本文主要是講解java中TreeMap,所以并沒有對紅黑樹進行非常深入的了解和研究,如果諸位想對其進行更加深入的研究Lz提供幾篇較好的博文: 1、紅黑樹系列集錦 2、紅黑樹數據結構剖析 3、紅黑樹
TreeMap的定義如下:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.SerializableTreeMap繼承AbstractMap,實現NavigableMap、Cloneable、Serializable三個接口。其中AbstractMap表明TreeMap為一個Map即支持key-value的集合, NavigableMap(更多)則意味著它支持一系列的導航方法,具備針對給定搜索目標返回最接近匹配項的導航方法 。 TreeMap中同時也包含了如下幾個重要的屬性:
//比較器,因為TreeMap是有序的,通過comparator接口我們可以對TreeMap的內部排序進行精密的控制 PRivate final Comparator<? super K> comparator; //TreeMap紅-黑節點,為TreeMap的內部類 private transient Entry<K,V> root = null; //容器大小 private transient int size = 0; //TreeMap修改次數 private transient int modCount = 0; //紅黑樹的節點顏色--紅色 private static final boolean RED = false; //紅黑樹的節點顏色--黑色 private static final boolean BLACK = true;對于葉子節點Entry是TreeMap的內部類,它有幾個重要的屬性:
//鍵 K key; //值 V value; //左孩子 Entry<K,V> left = null; //右孩子 Entry<K,V> right = null; //父親 Entry<K,V> parent; //顏色 boolean color = BLACK;注:前面只是開胃菜,下面是本篇博文的重中之重,在下面兩節我將重點講解treeMap的put()、delete()方法。通過這兩個方法我們會了解紅黑樹增加、刪除節點的核心算法。
在了解TreeMap的put()方法之前,我們先了解紅黑樹增加節點的算法。
紅黑樹在新增節點過程中比較復雜,復雜歸復雜它同樣必須要依據上面提到的五點規范,同時由于規則1、2、3基本都會滿足,下面我們主要討論規則4、5。假設我們這里有一棵最簡單的樹,我們規定新增的節點為N、它的父節點為P、P的兄弟節點為U、P的父節點為G。
對于新節點的插入有如下三個關鍵地方: 1、插入新節點總是紅色節點 。 2、如果插入節點的父節點是黑色, 能維持性質 。 3、如果插入節點的父節點是紅色, 破壞了性質. 故插入算法就是通過重新著色或旋轉, 來維持性質 。 為了保證下面的闡述更加清晰和根據便于參考,我這里將紅黑樹的五點規定再貼一遍:
1、每個節點都只能是紅色或者黑色 2、根節點是黑色 3、每個葉節點(NIL節點,空節點)是黑色的。 4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。 5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
若新插入的節點N沒有父節點,則直接當做根據節點插入即可,同時將顏色設置為黑色。(如圖一(1))
這種情況新節點N同樣是直接插入,同時顏色為紅色,由于根據規則四它會存在兩個黑色的葉子節點,值為null。同時由于新增節點N為紅色,所以通過它的子節點的路徑依然會保存著相同的黑色節點數,同樣滿足規則5。(如圖一(2))
(圖一) 對于這種情況若直接插入肯定會出現不平衡現象。怎么處理?P、U節點變黑、G節點變紅。這時由于經過節點P、U的路徑都必須經過G所以在這些路徑上面的黑節點數目還是相同的。但是經過上面的處理,可能G節點的父節點也是紅色,這個時候我們需要將G節點當做新增節點遞歸處理。
對于這種情況我們對新增節點N、P進行一次左旋轉。這里所產生的結果其實并沒有完成,還不是平衡的(違反了規則四),這是我們需要進行情況5的操作。

這種情況有可能是由于情況四而產生的,也有可能不是。對于這種情況先已P節點為中心進行右旋轉,在旋轉后產生的樹中,節點P是節點N、G的父節點。但是這棵樹并不規范,它違反了規則4,所以我們將P、G節點的顏色進行交換,使之其滿足規范。開始時所有的路徑都需要經過G其他們的黑色節點數一樣,但是現在所有的路徑改為經過P,且P為整棵樹的唯一黑色節點,所以調整后的樹同樣滿足規范5。
在TreeMap的put()的實現方法中主要分為兩個步驟,第一:構建排序二叉樹,第二:平衡二叉樹。 對于排序二叉樹的創建,其添加節點的過程如下: 1、以根節點為初始節點進行檢索。 2、與當前節點進行比對,若新增節點值較大,則以當前節點的右子節點作為新的當前節點。否則以當前節點的左子節點作為新的當前節點。 3、循環遞歸2步驟知道檢索出合適的葉子節點為止。 4、將新增節點與3步驟中找到的節點進行比對,如果新增節點較大,則添加為右子節點;否則添加為左子節點。 按照這個步驟我們就可以將一個新增節點添加到排序二叉樹中合適的位置。如下:
上面代碼中do{}代碼塊是實現排序二叉樹的核心算法,通過該算法我們可以確認新增節點在該樹的正確位置。找到正確位置后將插入即可,這樣做了其實還沒有完成,因為我知道TreeMap的底層實現是紅黑樹,紅黑樹是一棵平衡排序二叉樹,普通的排序二叉樹可能會出現失衡的情況,所以下一步就是要進行調整。fixAfterInsertion(e); 調整的過程務必會涉及到紅黑樹的左旋、右旋、著色三個基本操作。代碼如下:
對這段代碼的研究我們發現,其處理過程完全符合紅黑樹新增節點的處理過程。所以在看這段代碼的過程一定要對紅黑樹的新增節點過程有了解。在這個代碼中還包含幾個重要的操作。左旋(rotateLeft())、右旋(rotateRight())、著色(setColor())。
所謂左旋轉,就是將新增節點(N)當做其父節點(P),將其父節點P當做新增節點(N)的左子節點。即:G.left —> N ,N.left —> P。 左旋轉示意圖:
所謂右旋轉即,P.right —> G、G.parent —> P。
private void rotateRight(Entry<K,V> p) { if (p != null) { //將L設置為P的左子樹 Entry<K,V> l = p.left; //將L的右子樹設置為P的左子樹 p.left = l.right; //若L的右子樹不為空,則將P設置L的右子樹的父節點 if (l.right != null) l.right.parent = p; //將P的父節點設置為L的父節點 l.parent = p.parent; //如果P的父節點為空,則將L設置根節點 if (p.parent == null) root = l; //若P為其父節點的右子樹,則將L設置為P的父節點的右子樹 else if (p.parent.right == p) p.parent.right = l; //否則將L設置為P的父節點的左子樹 else p.parent.left = l; //將P設置為L的右子樹 l.right = p; //將L設置為P的父節點 p.parent = l; } }右旋的示意圖如下:
(圖片來自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html) 著色就是改變該節點的顏色,在紅黑樹中,它是依靠節點的顏色來維持平衡的。
private static <K,V> void setColor(Entry<K,V> p, boolean c) { if (p != null) p.color = c; }針對于紅黑樹的增加節點而言,刪除顯得更加復雜,使原本就復雜的紅黑樹變得更加復雜。同時刪除節點和增加節點一樣,同樣是找到刪除的節點,刪除之后調整紅黑樹。但是這里的刪除節點并不是直接刪除,而是通過走了“彎路”通過一種捷徑來刪除的:找到被刪除的節點D的子節點C,用C來替代D,不是直接刪除D,因為D被C替代了,直接刪除C即可。所以這里就將刪除父節點D的事情轉變為了刪除子節點C的事情,這樣處理就將復雜的刪除事件簡單化了。子節點C的規則是:右分支最左邊,或者 左分支最右邊的。

紅黑樹刪除節點同樣會分成幾種情況,這里是按照待刪除節點有幾個兒子的情況來進行分類: 1、沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。 2、只有一個兒子。那么把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。 3、有兩個兒子。這種情況比較復雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然后刪除子節點C即可。 下面就論各種刪除情況來進行圖例講解,但是在講解之前請允許我再次啰嗦一句,請時刻牢記紅黑樹的5點規定:
1、每個節點都只能是紅色或者黑色 2、根節點是黑色 3、每個葉節點(NIL節點,空節點)是黑色的。 4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。 5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。 (注:已經講三遍了,再不記住我就懷疑你是否適合搞IT了 O(∩_∩)O~)
誠然,既然刪除節點比較復雜,那么在這里我們就約定一下規則: 1、下面要講解的刪除節點一定是實際要刪除節點的后繼節點(N),如前面提到的C。 2、下面提到的刪除節點的樹都是如下結構,該結構所選取的節點是待刪除節點的右樹的最左邊子節點。這里我們規定真實刪除節點為N、父節點為P、兄弟節點為W兄弟節點的兩個子節點為X1、X2。如下圖(2.1)。
這種情況對該節點直接刪除即可,不會影響樹的結構。因為該節點為葉子節點它不可能存在子節點—–如子節點為黑,則違反黑節點數原則(規定5),為紅,則違反“顏色”原則(規定4)。 如上圖(2.2)。
這種情況處理也是非常簡單的,用子節點替代待刪除節點,然后刪除子節點即可。如上圖(2.3)
這種情況可能會稍微有點兒復雜。它需要找到一個替代待刪除節點(N)來替代它,然后刪除N即可。它主要分為四種情況。 1、N的兄弟節點W為紅色 2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。 3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。 4、N的兄弟w是黑色的,且w的右孩子時紅色的。
W為紅色,那么其子節點X1、X2必定全部為黑色,父節點P也為黑色。處理策略是:改變W、P的顏色,然后進行一次左旋轉。這樣處理就可以使得紅黑性質得以繼續保持。N的新兄弟new w是旋轉之前w的某個孩子,為黑色。這樣處理后將情況3.1、轉變為3.2、3.3、3.4中的一種。如下:
這種情況其父節點可紅可黑,由于W為黑色,這樣導致N子樹相對于其兄弟W子樹少一個黑色節點,這時我們可以將W置為紅色。這樣,N子樹與W子樹黑色節點一致,保持了平衡。如下:
針對這種情況是將節點W和其左子節點進行顏色交換,然后對W進行右旋轉處理。

交換W和父節點P的顏色,同時對P進行左旋轉操作。這樣就把左邊缺失的黑色節點給補回來了。同時將W的右子節點X2置黑。這樣左右都達到了平衡。

總結: 個人認為這四種情況比較難理解,首先他們都不是單一的某種情況,他們之間是可以進行互轉的。相對于其他的幾種情況,情況3.2比較好理解,僅僅只是一個顏色的轉變,通過減少右子樹的一個黑色節點使之保持平衡,同時將不平衡點上移至N與W的父節點,然后進行下一輪迭代。情況3.1,是將W旋轉將其轉成情況2、3、4情況進行處理。而情況3.3通過轉變后可以化成情況3.4來進行處理,從這里可以看出情況3.4應該最終結。情況3.4、右子節點為紅色節點,那么將缺失的黑色節點交由給右子節點,通過旋轉達到平衡。 通過上面的分析,我們已經初步了解了紅黑樹的刪除節點情況,相對于增加節點而言它確實是選的較為復雜。下面我將看到在Java TreeMap中是如何實現紅黑樹刪除的。
通過上面的分析我們確認刪除節點的步驟是:找到一個替代子節點C來替代P,然后直接刪除C,最后調整這棵紅黑樹。下面代碼是尋找替代節點、刪除替代節點。
private void deleteEntry(Entry<K,V> p) { modCount++; //修改次數 +1 size--; //元素個數 -1 /* * 被刪除節點的左子樹和右子樹都不為空,那么就用 p節點的中序后繼節點代替 p 節點 * successor(P)方法為尋找P的替代節點。規則是右分支最左邊,或者 左分支最右邊的節點 * ---------------------(1) */ if (p.left != null && p.right != null) { Entry<K,V> s = successor(p); p.key = s.key; p.value = s.value; p = s; } //replacement為替代節點,如果P的左子樹存在那么就用左子樹替代,否則用右子樹替代 Entry<K,V> replacement = (p.left != null ? p.left : p.right); /* * 刪除節點,分為上面提到的三種情況 * -----------------------(2) */ //如果替代節點不為空 if (replacement != null) { replacement.parent = p.parent; /* *replacement來替代P節點 */ //若P沒有父節點,則跟節點直接變成replacement if (p.parent == null) root = replacement; //如果P為左節點,則用replacement來替代為左節點 else if (p == p.parent.left) p.parent.left = replacement; //如果P為右節點,則用replacement來替代為右節點 else p.parent.right = replacement; //同時將P節點從這棵樹中剔除掉 p.left = p.right = p.parent = null; /* * 若P為紅色直接刪除,紅黑樹保持平衡 * 但是若P為黑色,則需要調整紅黑樹使其保持平衡 */ if (p.color == BLACK) fixAfterDeletion(replacement); } else if (p.parent == null) { //p沒有父節點,表示為P根節點,直接刪除即可 root = null; } else { //P節點不存在子節點,直接刪除即可 if (p.color == BLACK) //如果P節點的顏色為黑色,對紅黑樹進行調整 fixAfterDeletion(p); //刪除P節點 if (p.parent != null) { if (p == p.parent.left) p.parent.left = null; else if (p == p.parent.right) p.parent.right = null; p.parent = null; } } }(1)處是尋找替代節點replacement,其實現方法為successor()。如下:
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { if (t == null) return null; /* * 尋找右子樹的最左子樹 */ else if (t.right != null) { Entry<K,V> p = t.right; while (p.left != null) p = p.left; return p; } /* * 選擇左子樹的最右子樹 */ else { Entry<K,V> p = t.parent; Entry<K,V> ch = t; while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } }(2)處是刪除該節點過程。它主要分為上面提到的三種情況,它與上面的if…else if… else一一對應 。如下: 1、有兩個兒子。這種情況比較復雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然后刪除子節點C即可。 2、沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。 3、只有一個兒子。那么把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。 刪除完節點后,就要根據情況來對紅黑樹進行復雜的調整:fixAfterDeletion()。
private void fixAfterDeletion(Entry<K,V> x) { // 刪除節點需要一直迭代,知道 直到 x 不是根節點,且 x 的顏色是黑色 while (x != root && colorOf(x) == BLACK) { if (x == leftOf(parentOf(x))) { //若X節點為左節點 //獲取其兄弟節點 Entry<K,V> sib = rightOf(parentOf(x)); /* * 如果兄弟節點為紅色----(情況3.1) * 策略:改變W、P的顏色,然后進行一次左旋轉 */ if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateLeft(parentOf(x)); sib = rightOf(parentOf(x)); } /* * 若兄弟節點的兩個子節點都為黑色----(情況3.2) * 策略:將兄弟節點編程紅色 */ if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { /* * 如果兄弟節點只有右子樹為黑色----(情況3.3) * 策略:將兄弟節點與其左子樹進行顏色互換然后進行右轉 * 這時情況會轉變為3.4 */ if (colorOf(rightOf(sib)) == BLACK) { setColor(leftOf(sib), BLACK); setColor(sib, RED); rotateRight(sib); sib = rightOf(parentOf(x)); } /* *----情況3.4 *策略:交換兄弟節點和父節點的顏色, *同時將兄弟節點右子樹設置為黑色,最后左旋轉 */ setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(rightOf(sib), BLACK); rotateLeft(parentOf(x)); x = root; } } /** * X節點為右節點與其為做節點處理過程差不多,這里就不在累述了 */ else { Entry<K,V> sib = leftOf(parentOf(x)); if (colorOf(sib) == RED) { setColor(sib, BLACK); setColor(parentOf(x), RED); rotateRight(parentOf(x)); sib = leftOf(parentOf(x)); } if (colorOf(rightOf(sib)) == BLACK && colorOf(leftOf(sib)) == BLACK) { setColor(sib, RED); x = parentOf(x); } else { if (colorOf(leftOf(sib)) == BLACK) { setColor(rightOf(sib), BLACK); setColor(sib, RED); rotateLeft(sib); sib = leftOf(parentOf(x)); } setColor(sib, colorOf(parentOf(x))); setColor(parentOf(x), BLACK); setColor(leftOf(sib), BLACK); rotateRight(parentOf(x)); x = root; } } } setColor(x, BLACK); }這是紅黑樹在刪除節點后,對樹的平衡性進行調整的過程,其實現過程與上面四種復雜的情況一一對應,所以在這個源碼的時候一定要對著上面提到的四種情況看。
這篇博文確實是有點兒長,在這里非常感謝各位看客能夠靜下心來讀完,我想你通過讀完這篇博文一定收獲不小。同時這篇博文很大篇幅都在闡述紅黑樹的實現過程,對Java 的TreeMap聊的比較少,但是我認為如果理解了紅黑樹的實現過程,對TreeMap那是手到擒來,小菜一碟。 同時這篇博文我寫了四天,看了、參考了大量的博文。同時不免會有些地方存在借鑒之處,在這里對其表示感謝。LZ大二開始學習數據結構,自認為學的不錯,現在發現數據結構我還有太多的地方需要學習了,同時也再一次體味了算法的魅力!!!!
新聞熱點
疑難解答