上一篇容器元素比較Comparable&Comparator分析的時候,我們提到了TreeMap,但沒有去細致分析它,只是說明其在添加元素的時候可以進行比較,從而使得集合有序,但是怎么做的呢?我們下面來進行分析。
一、認識TreeMap
之前的文章講解了HashMap,它保證了以O(1)的時間復雜度進行增、刪、改、查,從存儲角度考慮,這兩種數據結構是非常優秀的。
盡管如此,HashMap還是有自己的局限性----它們不具備統計性能,或者說它們的統計性能時間復雜度並不是很好才更准確,所有的統計必須遍歷所有Entry,因此時間復雜度為O(N)。比如Map的Key有1、2、3、4、5、6、7,我現在要統計:
- 所有Key比3大的鍵值對有哪些
- Key最小的和Key最大的是哪兩個
就類似這些操作,HashMap做得比較差,此時我們可以使用TreeMap。TreeMap的Key按照自然順序進行排序或者根據創建映射時提供的Comparator接口進行排序。TreeMap為增、刪、改、查這些操作提供了log(N)的時間開銷,從存儲角度而言,這比HashMap的O(1)時間復雜度要差些;但是在統計性能上,TreeMap同樣可以保證log(N)的時間開銷,這又比HashMap的O(N)時間復雜度好不少。
因此總結而言:如果只需要存儲功能,使用HashMap是一種更好的選擇;如果還需要保證統計性能或者需要對Key按照一定規則進行排序,那么使用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)
注:由於本文主要是講解Java中TreeMap,所以並沒有對紅黑樹進行非常深入的了解和研究,如果想對其進行更加深入的研究提供幾篇較好的博文:
1、紅黑樹系列集錦
3、紅黑樹
三、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;
四、核心方法put 分析
分析put方法的過程,我們采用實例來進行分析,下面我們是我們寫的一段代碼:
package com.pony1223; import java.util.Map; import java.util.TreeMap; public class MapDemo { public static void main(String[] args) { TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>(); treeMap.put(10, "10"); treeMap.put(83, "83"); treeMap.put(15, "15"); treeMap.put(72, "72"); treeMap.put(20, "20"); treeMap.put(60, "60"); treeMap.put(30, "30"); treeMap.put(50, "50"); for (Map.Entry<Integer, String> entry : treeMap.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); } } }
我們打印結果發現:
事實再次證明TreeMap是有序的。
接下來的內容會給出插入每條數據之后紅黑樹的數據結構是什么樣子的。首先看一下treeMap的put方法的代碼實現:
public V put(K key, V value) { //用t表示二叉樹的當前節點 Entry<K,V> t = root; //t為null表示一個空樹,即TreeMap中沒有任何元素,直接插入 if (t == null) { //比較key值,個人覺得這句代碼沒有任何意義,空樹還需要比較、排序? compare(key, key); // type (and possibly null) check //將新的key-value鍵值對創建為一個Entry節點,並將該節點賦予給root root = new Entry<>(key, value, null); //容器的size = 1,表示TreeMap集合中存在一個元素 size = 1; //修改次數 + 1 modCount++; return null; } int cmp; //cmp表示key排序的返回結果 Entry<K,V> parent; //父節點 // split comparator and comparable paths Comparator<? super K> cpr = comparator; //指定的排序算法 //如果cpr不為空,則采用既定的排序算法進行創建TreeMap集合 if (cpr != null) { do { parent = t; //parent指向上次循環后的t //比較新增節點的key和當前節點key的大小 cmp = cpr.compare(key, t.key); //cmp返回值小於0,表示新增節點的key小於當前節點的key,則以當前節點的左子節點作為新的當前節點 if (cmp < 0) t = t.left; //cmp返回值大於0,表示新增節點的key大於當前節點的key,則以當前節點的右子節點作為新的當前節點 else if (cmp > 0) t = t.right; //cmp返回值等於0,表示兩個key值相等,則新值覆蓋舊值,並返回新值 else return t.setValue(value); } while (t != null); } //如果cpr為空,則采用默認的排序算法進行創建TreeMap集合 else { if (key == null) //key值為空拋出異常 throw new NullPointerException(); /* 下面處理過程和上面一樣 */ Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } //將新增節點當做parent的子節點 Entry<K,V> e = new Entry<>(key, value, parent); //如果新增節點的key小於parent的key,則當做左子節點 if (cmp < 0) parent.left = e; //如果新增節點的key大於parent的key,則當做右子節點 else parent.right = e; /* * 上面已經完成了排序二叉樹的的構建,將新增節點插入該樹中的合適位置 * 下面fixAfterInsertion()方法就是對這棵樹進行調整、平衡,具體過程參考上面的五種情況 */ fixAfterInsertion(e); //TreeMap元素數量 + 1 size++; //TreeMap容器修改次數 + 1 modCount++; return null; }
從這段代碼,先總結一下TreeMap添加數據的幾個步驟:
1.獲取根節點,根節點為空,產生一個根節點,將其着色為黑色,退出余下流程
2.獲取比較器,如果傳入的Comparator接口不為空,使用傳入的Comparator接口實現類進行比較;如果傳入的Comparator接口為空,將Key強轉為Comparable接口進行比較
3.從根節點開始逐一依照規定的排序算法進行比較,取比較值cmp,如果cmp=0,表示插入的Key已存在;如果cmp>0,取當前節點的右子節點;如果cmp<0,取當前節點的左子節點
4.排除插入的Key已存在的情況,第(3)步的比較一直比較到當前節點t的左子節點或右子節點為null,此時t就是我們尋找到的節點,cmp>0則准備往t的右子節點插入新節點,cmp<0則准備往t的左子節點插入新節點
5.new出一個新節點,默認為黑色,根據cmp的值向t的左邊或者右邊進行插入
6.插入之后進行修復,包括左旋、右旋、重新着色這些操作,讓樹保持平衡性
第1~第5步都沒有什么問題,紅黑樹最核心的應當是第6步插入數據之后進行的修復工作,對應的Java代碼是TreeMap中的fixAfterInsertion方法,下面看一下put每個數據之后TreeMap都做了什么操作,借此來理清TreeMap的實現原理。
put(10, "10")
首先是put(10, "10"),由於此時TreeMap中沒有任何節點,因此10為根且根節點為黑色節點,put(10, "10")之后的數據結構為:
put(83, "83")
接着是put(83, "83"),這一步也不難,83比10大,因此在10的右節點上,即執行上面代碼中do{}代碼塊,它是實現排序二叉樹的核心算法,通過該算法我們可以確認新增節點在該樹的正確位置。找到正確位置后將插入即可,但是由於83不是根節點,我知道TreeMap的底層實現是紅黑樹,紅黑樹是一棵平衡排序二叉樹,普通的排序二叉樹可能會出現失衡的情況,所以下一步就是要進行調整。fixAfterInsertion(e); 調整的過程務必會涉及到紅黑樹的左旋、右旋、着色三個基本操作。
1 /** 2 * 新增節點后的修復操作 3 * x 表示新增節點 4 */ 5 private void fixAfterInsertion(Entry<K,V> x) { 6 x.color = RED; //新增節點的顏色為紅色 7 8 //循環 直到 x不是根節點,且x的父節點不為紅色 9 while (x != null && x != root && x.parent.color == RED) { 10 //如果X的父節點(P)是其父節點的父節點(G)的左節點 11 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { 12 //獲取X的叔節點(U) 13 Entry<K,V> y = rightOf(parentOf(parentOf(x))); 14 //如果X的叔節點(U) 為紅色(情況三) 15 if (colorOf(y) == RED) { 16 //將X的父節點(P)設置為黑色 17 setColor(parentOf(x), BLACK); 18 //將X的叔節點(U)設置為黑色 19 setColor(y, BLACK); 20 //將X的父節點的父節點(G)設置紅色 21 setColor(parentOf(parentOf(x)), RED); 22 x = parentOf(parentOf(x)); 23 } 24 //如果X的叔節點(U為黑色);這里會存在兩種情況(情況四、情況五) 25 else { 26 //如果X節點為其父節點(P)的右子樹,則進行左旋轉(情況四) 27 if (x == rightOf(parentOf(x))) { 28 //將X的父節點作為X 29 x = parentOf(x); 30 //右旋轉 31 rotateLeft(x); 32 } 33 //(情況五) 34 //將X的父節點(P)設置為黑色 35 setColor(parentOf(x), BLACK); 36 //將X的父節點的父節點(G)設置紅色 37 setColor(parentOf(parentOf(x)), RED); 38 //以X的父節點的父節點(G)為中心右旋轉 39 rotateRight(parentOf(parentOf(x))); 40 } 41 } 42 //如果X的父節點(P)是其父節點的父節點(G)的右節點 43 else { 44 //獲取X的叔節點(U) 45 Entry<K,V> y = leftOf(parentOf(parentOf(x))); 46 //如果X的叔節點(U) 為紅色(情況三) 47 if (colorOf(y) == RED) { 48 //將X的父節點(P)設置為黑色 49 setColor(parentOf(x), BLACK); 50 //將X的叔節點(U)設置為黑色 51 setColor(y, BLACK); 52 //將X的父節點的父節點(G)設置紅色 53 setColor(parentOf(parentOf(x)), RED); 54 x = parentOf(parentOf(x)); 55 } 56 //如果X的叔節點(U為黑色);這里會存在兩種情況(情況四、情況五) 57 else { 58 //如果X節點為其父節點(P)的右子樹,則進行左旋轉(情況四) 59 if (x == leftOf(parentOf(x))) { 60 //將X的父節點作為X 61 x = parentOf(x); 62 //右旋轉 63 rotateRight(x); 64 } 65 //(情況五) 66 //將X的父節點(P)設置為黑色 67 setColor(parentOf(x), BLACK); 68 //將X的父節點的父節點(G)設置紅色 69 setColor(parentOf(parentOf(x)), RED); 70 //以X的父節點的父節點(G)為中心右旋轉 71 rotateLeft(parentOf(parentOf(x))); 72 } 73 } 74 } 75 //將根節點G強制設置為黑色 76 root.color = BLACK; 77 }
我們看第6行的代碼,它將默認的插入的那個節點着色成為紅色,這很好理解:
根據紅黑樹的性質(3),紅黑樹要求從根節點到葉子所有葉子節點上經過的黑色節點個數是相同的,因此如果插入的節點着色為黑色,那必然有可能導致某條路徑上的黑色節點數量大於其他路徑上的黑色節點數量,因此默認插入的節點必須是紅色的,以此來維持紅黑樹的性質(3).
當然插入節點着色為紅色節點后,有可能導致的問題是違反性質(2),即出現連續兩個紅色節點,這就需要通過旋轉操作去改變樹的結構,解決這個問題。
接着看
while (x != null && x != root && x.parent.color == RED)
的判斷,前兩個條件都滿足,但是因為83這個節點的父節點是根節點的,根節點是黑色節點,因此這個條件不滿足,while循環不進去,直接執行一次
root.color = BLACK;
行的代碼給根節點着色為黑色(因為在旋轉過程中有可能導致根節點為紅色,而紅黑樹的根節點必須是黑色,因此最后不管根節點是不是黑色,都要重新着色確保根節點是黑色的)。
那么put(83, "83")之后,整個樹的結構變為:
一、為根節點
若新插入的節點N沒有父節點,則直接當做根據節點插入即可,同時將顏色設置為黑色
二、父節點為黑色
這種情況新節點N同樣是直接插入,同時顏色為紅色,由於根據規則四它會存在兩個黑色的葉子節點,值為null。同時由於新增節點N為紅色,所以通過它的子節點的路徑依然會保存着相同的黑色節點數,同樣滿足規則5。
在看put(15, "15")之前,必須要先過一下fixAfterInsertion方法。第11行~第41行的代碼和第43行~第73行的代碼是一樣的,無非一個是操作左子樹另一個是操作右子樹而已,因此就看前一半:
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行的判斷注意一下,用語言描述出來就是:判斷當前節點的父節點與當前節點的父節點的父節點的左子節點是否同一個節點。翻譯一下就是:當前節點是否左子節點插入.
在上面代碼中說到的情況三、四、五的意思是;
三、若父節點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。
總結:對於第四種情況和第五種情況而言這兩種插入方式的處理是不同的,區別是第四種情況插入多一步左旋操作。能看出,紅黑樹的插入最多只需要進行兩次旋轉,不管這棵紅黑樹多么復雜,都可以根據這五種情況來進行生成。
其中左邊的是左子樹外側插入,右邊的是左子樹內側插入
put(15, "15")
看完fixAfterInsertion方法流程之后,繼續添加數據,這次添加的是put(15, "15"),15比10大且比83小,因此15最終應當是83的左子節點,默認插入的是紅色節點,因此首先將15作為紅色節點插入83的左子節點后的結構應當是:
但是顯然這里違反了紅黑樹的性質(2),即連續出現了兩個紅色節點,因此此時必須進行旋轉。回看前面fixAfterInsertion的流程,上面演示的是左子樹插入流程,右子樹一樣,可以看到這是右子樹內側插入,需要進行兩次旋轉操作:
- 對新插入節點的父節點進行一次右旋操作
- 新插入節點的父節點着色為黑色,新插入節點的祖父節點着色為紅色
- 對新插入節點的祖父節點進行一次左旋操作
旋轉是紅黑樹中最難理解也是最核心的操作,右旋和左旋是對稱的操作,我個人的理解,以右旋為例,對某個節點x進行右旋,其實質是:
- 降低左子樹的高度,增加右子樹的高度
- 將x變為當前位置的右子節點
左旋是同樣的道理,在旋轉的時候一定要記住這兩句話,這將會幫助我們清楚地知道在不同的場景下旋轉如何進行。
先看一下"對新插入節點的父節點進行一次右旋操作",源代碼為rotateRight方法:
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; } }
左旋與右旋是一個對稱的操作,大家可以試試看把右圖的b節點進行左旋,就變成了左圖了。這里多說一句,旋轉一定要說明是對哪個節點進行旋轉,網上看很多文章講左旋、右旋都是直接說旋轉之后怎么樣怎么樣,我認為脫離具體的節點講旋轉是沒有任何意義的。
這里可能會有的一個問題是:b有左右兩個子節點分別為d和e,為什么右旋的時候要將右子節點e拿到a的左子節點而不是b的左子節點d?
一個很簡單的解釋是:如果將b的左子節點d拿到a的左子節點,那么b右旋后右子節點指向a,b原來的右子節點e就成為了一個游離的節點,游離於整個數據結構之外。
回到實際的例子,對83這個節點進行右旋之后還有一次着色操作(2),分別是將x的父節點着色為黑色,將x的祖父節點着色為紅色,然后對節點10進行一次左旋操作(3),左旋之后的結構為:
put(72, "72")
put(72, "72")就很簡單了,72是83的左子節點,由於72的父節點以及叔父節點都是紅色節點,因此直接將72的父節點83、將72的叔父節點10着色為黑色即可,72這個節點着色為紅色,即滿足紅黑樹的特性,插入72之后的結構圖為:
put(20, "20")
put(20, "20"),插入的位置應當是72的左子節點,默認插入紅色,插入之后的結構圖為:
問題很明顯,出現了連續兩個紅色節點,20的插入位置是一種左子樹外側插入的場景,因此只需要進行着色+對節點83進行一次右旋即可,着色+右旋之后數據結構變為:
put(60, "60")
下面進行put(60, "60")操作,節點60插入的位置是節點20的右子節點,由於節點60的父節點與叔父節點都是紅色節點,因此只需要將節點60的父節點與叔父節點着色為黑色,將節點60的組父節點着色為紅色即可。
那么put(60, "60")之后的結構為:
put(30, "30")
put(30, "30"),節點30應當為節點60的左子節點,因此插入節點30之后應該是這樣的:
顯然這里違反了紅黑樹性質即連續出現了兩個紅色節點,因此這里要進行旋轉。
put(30, "30")的操作和put(15, "15")的操作類似,同樣是右子樹內側插入的場景,那么需要進行兩次旋轉:
- 對節點30的父節點節點60進行一次右旋
- 右旋之后對節點60的祖父節點20進行一次左旋
右旋+着色+左旋之后,put(30, "30")的結果應當為:
put(50, "50")
private void rotateLeft(Entry<K,V> p) { if (p != null) { //獲取P的右子節點,其實這里就相當於新增節點N(情況四而言) Entry<K,V> r = p.right; //將R的左子樹設置為P的右子樹 p.right = r.left; //若R的左子樹不為空,則將P設置為R左子樹的父親 if (r.left != null) r.left.parent = p; //將P的父親設置R的父親 r.parent = p.parent; //如果P的父親為空,則將R設置為跟節點 if (p.parent == null) root = r; //如果P為其父節點(G)的左子樹,則將R設置為P父節點(G)左子樹 else if (p.parent.left == p) p.parent.left = r; //否則R設置為P的父節點(G)的右子樹 else p.parent.right = r; //將P設置為R的左子樹 r.left = p; //將R設置為P的父節點 p.parent = r; } }
五、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、從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
誠然,既然刪除節點比較復雜,那么在這里我們就約定一下規則:
1、下面要講解的刪除節點一定是實際要刪除節點的后繼節點(N),如前面提到的C。
2、下面提到的刪除節點的樹都是如下結構,該結構所選取的節點是待刪除節點的右樹的最左邊子節點。這里我們規定真實刪除節點為N、父節點為P、兄弟節點為W兄弟節點的兩個子節點為X1、X2。
刪除元素的過程和普通二叉搜索樹的搜索過程大體也比較類似,首先是根據待刪除節點的情況進行分析:
1. 待刪除節點沒有子節點, 則直接刪除該節點。如下圖:
2. 待刪除節點有一個子節點,則用該子節點替換它的父節點:
3. 待刪除節點有兩個子節點,則取它的后繼節點替換它,並刪除這個后繼節點原來的位置。它可能有種情況
刪除后的調整
刪除元素之后的調整和前面的插入元素調整的過程比起來更復雜。它不是一個簡單的在原來過程中取反。我們先從一個最基本的點開始入手。首先一個,我們要進行調整的這個點肯定是因為我們要刪除的這個點破壞了紅黑樹的本質特性。而如果我們刪除的這個點是紅色的,則它肯定不會破壞里面的屬性。因為從前面刪除的過程來看,我們這個要刪除的點是已經在瀕臨葉節點的附近了,它要么有一個子節點,要么就是一個葉節點。如果它是紅色的,刪除了,從上面的節點到葉節點所經歷的黑色節點沒有變化。所以,這里的一個前置條件就是待刪除的節點是黑色的。
在前面的那個前提下,我們要調整紅黑樹的目的就是要保證,這個原來是黑色的節點被刪除后,我們要通過一定的變化,使得他們仍然是合法的紅黑樹。我們都知道,在一個黑色節點被刪除后,從上面的節點到它所在的葉節點路徑所經歷的黑色節點就少了一個。我們需要做一些調整,使得它少的這個在后面某個地方能夠補上。
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種情況是在當前節點是父節點的左子節點的條件下進行的。如果當前節點是父節點的右子節點,則可以對應的做對稱的操作處理,過程也是一樣的。
其他
TreeMap的紅黑樹實現當然也包含其他部分的代碼實現,如用於查找元素的getEntry方法,取第一個和最后一個元素的getFirstEntry, getLastEntry方法以及求前驅和后繼的predecesor, successor方法。這些方法的實現和普通二叉搜索樹的實現沒什么明顯差別。這里就忽略不討論了。這里還有一個有意思的方法實現,就是buildFromSorted方法。它的實現過程並不復雜,不過經常被作為面試的問題來討論。
六、小節
這篇博文確實是有點兒長,在這里非常感謝各位看客能夠靜下心來讀完,我想你通過讀完這篇博文一定收獲不小。
同時這篇博文我寫的過程中,看了、參考了大量的博文。同時不免會有些地方存在借鑒之處,在這里對其表示感謝。
另外,我想說的是,重點要掌握的是put方法即可,其它如果精力足夠可以細細品讀。
參考資料:
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
7、紅黑樹概念、紅黑樹的插入及旋轉操作:http://www.cnblogs.com/xrq730/p/6867924.html
8、treemap:http://blog.csdn.net/chenssy/article/details/26668941