原文出自:http://cmsblogs.com/?p=1013。尊重作者的成果,轉載請註明出處!
個人站點:http://cmsblogs.com
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------TreeMap的實現是紅黑樹算法的實現,所以要了解TreeMap就必須對紅黑樹有一定的瞭解,其實這篇博文的名字叫做:根據紅黑樹的算法來分析TreeMap的實現,但是爲了與Java提高篇系列博文保持一致還是叫做TreeMap比較好。通過這篇博文你可以獲得如下知識點:
1、紅黑樹的基本概念。
2、紅黑樹增加節點、刪除節點的實現過程。
3、紅黑樹左旋轉、右旋轉的複雜過程。
4、Java 中TreeMap是如何通過put、deleteEntry兩個來實現紅黑樹增加、刪除節點的。
我想通過這篇博文你對TreeMap一定有了更深的認識。好了,下面先簡單普及紅黑樹知識。
一、紅黑樹簡介
紅黑樹又稱紅-黑二叉樹,它首先是一顆二叉樹,它具體二叉樹所有的特性。同時紅黑樹更是一顆自平衡的排序二叉樹。
我們知道一顆基本的二叉樹他們都需要滿足一個基本性質--即樹中的任何節點的值大於它的左子節點,且小於它的右子節點。按照這個基本性質使得樹的檢索效率大大提高。我們知道在生成二叉樹的過程是非常容易失衡的,最壞的情況就是一邊倒(只有右/左子樹),這樣勢必會導致二叉樹的檢索效率大大降低(O(n)),所以爲了維持二叉樹的平衡,大牛們提出了各種實現的算法,如:AVL,SBT,伸展樹,TREAP ,紅黑樹等等。
平衡二叉樹必須具備如下特性:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。也就是說該二叉樹的任何一個等等子節點,其左右子樹的高度都相近。
紅黑樹顧名思義就是節點是紅色或者黑色的平衡二叉樹,它通過顏色的約束來維持着二叉樹的平衡。對於一棵有效的紅黑樹二叉樹而言我們必須增加如下規則:
1、每個節點都只能是紅色或者黑色
2、根節點是黑色
3、每個葉節點(NIL節點,空節點)是黑色的。
4、如果一個結點是紅的,則它兩個子節點都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。
5、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這棵樹大致上是平衡的。因爲操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。所以紅黑樹它是複雜而高效的,其檢索效率O(log n)。下圖爲一顆典型的紅黑二叉樹。
對於紅黑二叉樹而言它主要包括三大基本操作:左旋、右旋、着色。
左旋 右旋
(圖片來自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html)
本節參考文獻:http://baike.baidu.com/view/133754.htm?fr=aladdin-----百度百科
注:由於本文主要是講解Java中TreeMap,所以並沒有對紅黑樹進行非常深入的瞭解和研究,如果諸位想對其進行更加深入的研究Lz提供幾篇較好的博文:
1、紅黑樹系列集錦
3、紅黑樹
二、TreeMap數據結構
>>>>>>迴歸主角:TreeMap<<<<<<
TreeMap的定義如下:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap繼承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()方法
在瞭解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和P的兄弟節點U都爲紅色
對於這種情況若直接插入肯定會出現不平衡現象。怎麼處理?P、U節點變黑、G節點變紅。這時由於經過節點P、U的路徑都必須經過G所以在這些路徑上面的黑節點數目還是相同的。但是經過上面的處理,可能G節點的父節點也是紅色,這個時候我們需要將G節點當做新增節點遞歸處理。
四、若父節點P爲紅色,叔父節點U爲黑色或者缺少,且新增節點N爲P節點的右孩子
對於這種情況我們對新增節點N、P進行一次左旋轉。這裏所產生的結果其實並沒有完成,還不是平衡的(違反了規則四),這是我們需要進行情況5的操作。
五、父節點P爲紅色,叔父節點U爲黑色或者缺少,新增節點N爲父節點P左孩子
這種情況有可能是由於情況四而產生的,也有可能不是。對於這種情況先已P節點爲中心進行右旋轉,在旋轉後產生的樹中,節點P是節點N、G的父節點。但是這棵樹並不規範,它違反了規則4,所以我們將P、G節點的顏色進行交換,使之其滿足規範。開始時所有的路徑都需要經過G其他們的黑色節點數一樣,但是現在所有的路徑改爲經過P,且P爲整棵樹的唯一黑色節點,所以調整後的樹同樣滿足規範5。
上面展示了紅黑樹新增節點的五種情況,這五種情況涵蓋了所有的新增可能,不管這棵紅黑樹多麼複雜,都可以根據這五種情況來進行生成。下面就來分析Java中的TreeMap是如何來實現紅黑樹的。
TreeMap put()方法實現分析
在TreeMap的put()的實現方法中主要分爲兩個步驟,第一:構建排序二叉樹,第二:平衡二叉樹。
對於排序二叉樹的創建,其添加節點的過程如下:
1、以根節點爲初始節點進行檢索。
2、與當前節點進行比對,若新增節點值較大,則以當前節點的右子節點作爲新的當前節點。否則以當前節點的左子節點作爲新的當前節點。
3、循環遞歸2步驟知道檢索出合適的葉子節點爲止。
4、將新增節點與3步驟中找到的節點進行比對,如果新增節點較大,則添加爲右子節點;否則添加爲左子節點。
按照這個步驟我們就可以將一個新增節點添加到排序二叉樹中合適的位置。如下:
對這段代碼的研究我們發現,其處理過程完全符合紅黑樹新增節點的處理過程。所以在看這段代碼的過程一定要對紅黑樹的新增節點過程有了解。在這個代碼中還包含幾個重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。
左旋:rotateLeft()
所謂左旋轉,就是將新增節點(N)當做其父節點(P),將其父節點P當做新增節點(N)的左子節點。即:G.left ---> N ,N.left ---> P。
右旋:rotateRight()
所謂右旋轉即,P.right ---> G、G.parent ---> P。
左旋、右旋的示意圖如下:
(左旋) (右旋)
(圖片來自:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html)
着色:setColor()
着色就是改變該節點的顏色,在紅黑樹中,它是依靠節點的顏色來維持平衡的。
四、TreeMap delete()方法
紅黑樹刪除節點
針對於紅黑樹的增加節點而言,刪除顯得更加複雜,使原本就複雜的紅黑樹變得更加複雜。同時刪除節點和增加節點一樣,同樣是找到刪除的節點,刪除之後調整紅黑樹。但是這裏的刪除節點並不是直接刪除,而是通過走了“彎路”通過一種捷徑來刪除的:找到被刪除的節點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的右孩子時紅色的。
情況3.1、N的兄弟節點W爲紅色
W爲紅色,那麼其子節點X1、X2必定全部爲黑色,父節點P也爲黑色。處理策略是:改變W、P的顏色,然後進行一次左旋轉。這樣處理就可以使得紅黑性質得以繼續保持。N的新兄弟new w是旋轉之前w的某個孩子,爲黑色。這樣處理後將情況3.1、轉變爲3.2、3.3、3.4中的一種。如下:
情況3.2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。
這種情況其父節點可紅可黑,由於W爲黑色,這樣導致N子樹相對於其兄弟W子樹少一個黑色節點,這時我們可以將W置爲紅色。這樣,N子樹與W子樹黑色節點一致,保持了平衡。如下
將W由黑轉變爲紅,這樣就會導致新節點new N相對於它的兄弟節點會少一個黑色節點。但是如果new x爲紅色,我們直接將new x轉變爲黑色,保持整棵樹的平衡。否則情況3.2 會轉變爲情況3.1、3.3、3.4中的一種。
情況3.3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。
針對這種情況是將節點W和其左子節點進行顏色交換,然後對W進行右旋轉處理。
此時N的新兄弟X1(new w)是一個有紅色右孩子的黑結點,於是將情況3轉化爲情況4.
情況3.4、N的兄弟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中是如何實現紅黑樹刪除的。
TreeMap deleteEntry()方法實現分析
通過上面的分析我們確認刪除節點的步驟是:找到一個替代子節點C來替代P,然後直接刪除C,最後調整這棵紅黑樹。下面代碼是尋找替代節點、刪除替代節點。
(1)除是尋找替代節點replacement,其實現方法爲successor()。如下:
(2)處是刪除該節點過程。它主要分爲上面提到的三種情況,它與上面的if…else if… else一一對應 。如下:
1、有兩個兒子。這種情況比較複雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然後刪除子節點C即可。
2、沒有兒子,即爲葉結點。直接把父結點的對應兒子指針設爲NULL,刪除兒子結點就OK了。
3、只有一個兒子。那麼把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。
刪除完節點後,就要根據情況來對紅黑樹進行復雜的調整:fixAfterDeletion()。
這是紅黑樹在刪除節點後,對樹的平衡性進行調整的過程,其實現過程與上面四種複雜的情況一一對應,所以在這個源碼的時候一定要對着上面提到的四種情況看。
五、寫在最後
這篇博文確實是有點兒長,在這裏非常感謝各位看客能夠靜下心來讀完,我想你通過讀完這篇博文一定收穫不小。同時這篇博文很大篇幅都在闡述紅黑樹的實現過程,對Java 的TreeMap聊的比較少,但是我認爲如果理解了紅黑樹的實現過程,對TreeMap那是手到擒來,小菜一碟。
同時這篇博文我寫了四天,看了、參考了大量的博文。同時不免會有些地方存在借鑑之處,在這裏對其表示感謝。LZ大二開始學習數據結構,自認爲學的不錯,現在發現數據結構我還有太多的地方需要學習了,同時也再一次體味了算法的魅力!!!!
參考資料:
1、紅黑樹數據結構剖析:http://www.cnblogs.com/fanzhidongyzby/p/3187912.html
2、紅黑二叉樹詳解及理論分析 :http://blog.csdn.net/kartorz/article/details/8865997
3、教你透徹瞭解紅黑樹 :blog.csdn.net/v_july_v/article/details/6105630
4、經典算法研究系列:五、紅黑樹算法的實現與剖析 :http://blog.csdn.net/v_JULY_v/article/details/6109153
5、示例,紅黑樹插入和刪除過程:http://saturnman.blog.163.com/blog/static/557611201097221570/
6、紅黑二叉樹詳解及理論分析 :http://blog.csdn.net/kartorz/article/details/8865997
原文出自:http://cmsblogs.com/?p=1013。尊重作者的成果,轉載請註明出處!
個人站點:http://cmsblogs.com