1、概述node
在前一篇中咱們回顧了紅黑樹的特色及添加的處理,能夠得知紅黑樹首先是一個二叉查找樹,在此基礎上經過增長節點顏色的約束來使得紅黑樹近似平衡。當咱們添加或者刪除節點時,咱們須要對樹進行調整以使其從新知足紅黑樹。這涉及到節點顏色的變化及部分節點的旋轉。關於節點的旋轉,以及添加時的處理咱們已經介紹完了,因此本文重點介紹紅黑樹的刪除。算法
2、紅黑樹的特色spa
在介紹刪除時,咱們仍是再來回顧一下紅黑樹的五個特色,以下:指針
1)節點的顏色是紅色或者黑色code
2)樹的根節點爲黑色blog
3)樹的葉子節點爲黑色繼承
4)若是一個節點的顏色爲紅色,則其兩個兒子節點必爲黑色element
5)任一節點的兩個子樹,各路徑中的黑色節點的個數相等。同步
對節點的刪除後,這5個性質可能會被破壞,因此刪除以後,也須要經過一系列的處理來使整個樹從新變爲紅黑樹。it
3、紅黑樹的刪除過程
紅黑樹的刪除算法主要有三個步驟
3.1 找到待刪除的節點,這是一個二叉查找樹的查找過程,比較簡單。
3.2 在找到該節點的狀況下,根據該節點孩子的狀況,又能夠分紅三種狀況:
1) 該節點有兩個非葉子節點的孩子
2)該節點有一個非葉子節點的孩子
3)該節點只有葉子節點
在這裏,咱們所說的葉子節點指的是空節點。那麼根據這三種狀況,咱們須要作不一樣的處理。
針對於狀況1,咱們須要找到當前節點的後繼節點,後繼節點的定義是該節點的子樹中,第一個沒有左孩子(或左孩子爲葉子節點)的節點。找到該節點後,咱們須要把該節點的值同步給當前節點,並轉而去刪除其後繼節點。
這樣作的好處就是,其後繼節點最多隻可能有一個孩子(並且是右孩子)。由於根據後繼節點的定義,其左孩子必爲葉子節點。那這樣對於其後續節點再作刪除處理的話,就不會有狀況1的存在了,而只會有狀況2和3的存在。
咱們經過示意圖來演示以下:
經過上面的處理,咱們將刪除改成對節點24的刪除,但24刪除以後其數據就沒有了,因此咱們把其內容放到原來待刪除的節點,22.5上。
針對於狀況2,被刪除的節點有一個孩子,咱們須要找到其唯一的兒子,並將被刪除的節點從樹中移除,並將這個唯一的兒子做爲待處理的起始點,進行樹調整。
示意圖以下:
針對於狀況3,咱們直接把當前節點做爲起始點進行樹的紅黑性質調整,只是調整結束後,因爲以前並未進行節點的刪除,因此須要將當前節點再刪除掉。
3.3 對刪除節點後樹進行調整
在刪除節點以後,樹的紅黑性質可能會被破壞,當被破壞後咱們須要對樹進行調整。因此,咱們要先明確,刪除一個節點以後,紅黑樹的哪些性質可能會被破壞。
a) 當被刪除的節點是紅色時,好比上圖中的27,這個時候其所在的路徑中的黑節點個數不會發生變化,即性質5不會被破壞;因爲其是紅節點,因此其父親必定是黑色,因此也不會出現父子都爲紅色的狀況,因此性質4不會被破壞;另外紅節點必定不是根節點,因此性質2也不會被破壞;另外性質1和性質3不可能被改變。
因此,當刪除一個紅節點,樹的5個性質都不會發生變化,這個樹仍然是一個紅黑樹,不須要作任何處理。
b) 當被刪除的節點是黑色的,那麼可能出現如下一些狀況。
b1)當被刪除的是根節點時,且根節點只有一個紅兒子,則刪除後該節點的紅兒子成爲了樹中的唯一節點,且爲紅色,這就破壞了性質2。
b2)當被刪除的是非根節點,則該節點必定有兄弟節點,要否則由於該節點所在的路徑的黑節點個數就比另外一個分支多一個了。那正是由於這樣的緣由,若是該節點被刪除,則該節點所在分支的黑節點個數就比其兄弟節點所在的路徑的黑節點個數少1個了。因此非根節點的狀況下,性質5確定會被破壞。
b3)當被刪除的是非根節點,且父子都爲紅節點的狀況下,當前節點刪除後,該節點的孩子成爲了該節點的父節點的孩子。如上圖所求的節點24和25.
因爲會出現這三種狀況,因此當被刪除的節點是黑色時,咱們須要對樹的紅黑性進行恢復。咱們把上面的狀況分爲兩部分,即被刪除的節點是否是根,這樣來分開處理。若是是根就比較好處理,這種狀況下,若是有紅兒子則把紅兒子變成黑色就結束了,沒有紅兒子則什麼都不用作。
那麼對於非根的狀況就比較複雜了,前面說過,這種狀況下,有兩個推論,就是黑節點的個數比其它路徑少1,且該節點必定有兄弟節點。咱們調整也能夠基於這個前提來作。紅黑樹的提出者給出了一種算法,這個算法接收一個節點做爲調整的起始節點,前面說了,這個節點多是被刪除節點的子節點,也多是被刪除節點自己(在沒有兒子節點的狀況下)。
爲了便於循環處理,算法假定當前輸入的節點所在的子樹是知足紅黑樹的,但咱們知道其實輸入節點所在路徑的黑節點個數比其它路徑少1個,爲了解決這個問題,算法假定當前節點具有特殊性,該節點與其它普通的節點相比,多了一重「黑色」,則該節點多是「黑黑」或者「紅黑」,這樣就抹平了黑節點少1的問題。可是這種假設只是爲了便於咱們理解,並非真正的有這個顏色,在這個假定條件下,咱們能夠作出一些調整使得整個樹知足紅黑性,並把缺乏的這一重黑色給補回來。
基於這個前提,算法對可能出現的幾種狀況給出了處理方案。
[1]. 輸入節點爲紅色,兄弟節點顏色任意
這個比較簡單,直接把節點變成黑色便可,以下所求:
[2] 輸入節點爲黑色
[2.1] 兄弟節點爲紅色
這種狀況下,很顯然,兄弟的父親和兒子都是黑色的。這個時候咱們要想辦法把兄弟節點變爲黑色,比較好的方案就是在父節點進行左旋,這樣原兄弟節點的左孩子就成了父親節點的右孩子,也就是該節點的兄弟,實現了兄弟節點從紅節點到黑節點的轉化。
在旋轉前,須要將兄弟節點變爲黑色,原父親節點變爲紅色,才能保持黑節點個數不變。示例以下:
轉化完成以後,則兄弟節點變成了黑色,當前節點位置不變,繼續處理。
[2.2] 兄弟節點爲黑色
[2.2.1] 兄弟節點的兩個兒子都是黑色
這種狀況下,因爲兄弟的兩個兒子都爲黑色,因此咱們有機會把兄弟設置爲紅色,並將當前節點設置爲父親節點,繼續算法。這樣,父節點就具有了這個神奇的特性,因爲它多了一重黑色,因此雖然兄弟變紅了,但兄弟分支上的黑節點個數不變,因爲原輸入節點已再也不是特殊節點,因此原輸入節點也就失去了原來多的那一重黑色,因此原輸入節點所在的路徑黑節點個數也保持不變,說明了這種處理不會影響紅黑樹的性質。可是咱們把當前節點已經提高到了父節點,若是一直是這樣的狀況,則必定能到根節點以結束。
這個過程的圖示以下:
如上圖所示,若是[2.1.1]是由[2.1]經過旋轉轉換而來的話,則當前節點就變成了紅色節點,下一次處理時直接將其變紅便可。
若是兩個兒子不都是黑色,則兄弟節點的孩子中必有一個是紅色。在上圖中,爲了達到抹去節點24中的額外的一重黑色,咱們要在保持父節點顏色不變的基礎上,爲其增長一個黑色節點,要作到這一點,必須以父節點爲支點進行左旋轉,這樣節點24就會降低一級,其所在的路徑上便會新增一個節點。因爲左旋,且須要人爲新增一個黑節點,必然致使原兄弟節點的路徑上顏色也發生變化,爲了便於着色,咱們須要兄弟節點的右孩子爲紅色。
若是兄弟節點的右孩子自己就爲紅色,固然直接處理便可,若是右孩子爲黑色,則左孩子必爲紅色,對這種狀況,咱們須要多作一次旋轉。
[2.2.2] 兄弟節點的左孩子是紅色,右孩子是黑色
這種狀況下,根據前面的分析,咱們要作一次旋轉讓右孩子爲紅色,這個也比較簡單,操做以下:
這樣就順利地完成了兄弟節點的左孩子爲紅色變爲右孩子爲紅色的過程。當右孩子爲紅色時,就能夠進行上面討論的處理了。
[2.2.3] 兄弟節點的右孩子爲紅色,左孩子顏色任意
這種狀況下,前面說過咱們要想辦法給當前節點所在的路徑增長一個黑色節點,因此要將父節點左旋,同時改變一些節點的顏色,具體操做爲:
將父節點變爲黑色(此即爲新增的節點)
將兄弟節點的顏色變爲父節點的顏色(父節點左旋時,兄弟節點要上移成爲新的父親節點,因此它要繼承原父親節點的顏色),以彌補父節點下移的問題
兄弟節點的右兒子顏色變爲兄弟節點的顏色(兄弟節點爲黑色,因此右兒子天然也爲黑色),以彌補兄弟節點顏色丟失的問題。
具體圖示以下:
經過以上的處理,咱們會發現除了當前節點24所在的路徑的黑節點個數增長了以外,其它路徑的黑節點個數仍然保持了一致,這樣就能夠將當前節點的
額外的一重黑色去掉,整個樹就從新保持紅黑樹性質了。
至此,整個刪除後節點的調整就到此結束了。
4、代碼示例
這裏給出TreeMap裏關於節點的刪除處理。
4.1 刪除節點
本部分是刪除節點的方法,不包括調整,以下:
private void deleteEntry(Entry<K,V> p) { modCount++; size--; // If strictly internal, copy successor's element to p and then make p // point to successor. if (p.left != null && p.right != null) { //這種狀況下找後繼節點, Entry<K,V> s = successor(p); //存儲後繼節點的值 p.key = s.key; p.value = s.value; //P指針指向後繼節點,轉爲對後繼節點進行刪除 p = s; } // p has 2 children // Start fixup at replacement node, if it exists. //後繼節點的兒子節點,只可能有一個 Entry<K,V> replacement = (p.left != null ? p.left : p.right); //由於p只有一個孩子,因此不用兩個都處理 if (replacement != null) { //有孩子,則刪除節點 replacement.parent = p.parent; if (p.parent == null) root = replacement; else if (p == p.parent.left) p.parent.left = replacement; else p.parent.right = replacement; p.left = p.right = p.parent = null; //若是移除的是紅節點則無所謂,不用處理,若是是黑節點則要處理 if (p.color == BLACK) fixAfterDeletion(replacement); //從被刪除的兒子節點開始調整 } else if (p.parent == null) { // return if we are the only node. root = null;//刪除了根節點 } else { // No children. Use self as phantom replacement and unlink. // 即沒有左節點也沒有右節點, 則自身做爲參數節點 if (p.color == BLACK) fixAfterDeletion(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; } } }
4.2 具體的調整操做
1 /** From CLR */ 2 private void fixAfterDeletion(Entry<K,V> x) { 3 while (x != root && colorOf(x) == BLACK) { 4 if (x == leftOf(parentOf(x))) { 5 //X爲左孩子 6 //兄弟節點 7 Entry<K,V> sib = rightOf(parentOf(x)); 8 9 //2.1 兄弟節點爲紅色,則經過調整,把兄弟節點變黑 10 if (colorOf(sib) == RED) { 11 12 setColor(sib, BLACK);//兄弟節點變黑色 13 setColor(parentOf(x), RED); //父節點變紅 14 rotateLeft(parentOf(x)); //右旋轉 15 sib = rightOf(parentOf(x)); //從新定義兄弟節點 16 } 17 18 //2.2 兄弟節點爲黑色 19 if (colorOf(leftOf(sib)) == BLACK && 20 colorOf(rightOf(sib)) == BLACK) { 21 //2.2.1 兄弟節點的兩個兒子都爲黑色 22 setColor(sib, RED); //兄弟節點變紅 23 x = parentOf(x); //當前節點上移 24 } else { 25 26 if (colorOf(rightOf(sib)) == BLACK) { 27 //2.2.2 兄弟節點的右孩子爲黑色,則要將其調整爲紅色 28 setColor(leftOf(sib), BLACK); //左孩子設置爲黑色 29 setColor(sib, RED); //兄弟節點設置爲紅色 30 rotateRight(sib); //對兄弟節點右旋 31 sib = rightOf(parentOf(x)); //從新定義兄弟節點 32 } 33 34 //2.2.3 兄弟節點的右孩子爲紅色 35 setColor(sib, colorOf(parentOf(x)));//兄弟節點的顏色變爲其父親的顏色 36 setColor(parentOf(x), BLACK); //父親節點的顏色變爲黑色 37 setColor(rightOf(sib), BLACK); //兄弟節點的右兒子變爲黑色 38 rotateLeft(parentOf(x)); //在父節點進行左旋 39 x = root; //整個樹已經恢復爲紅黑樹,x = root,結束循環。 40 } 41 } else { 42 // 對稱的操做 43 Entry<K,V> sib = leftOf(parentOf(x)); 44 45 if (colorOf(sib) == RED) { 46 setColor(sib, BLACK); 47 setColor(parentOf(x), RED); 48 rotateRight(parentOf(x)); 49 sib = leftOf(parentOf(x)); 50 } 51 52 if (colorOf(rightOf(sib)) == BLACK && 53 colorOf(leftOf(sib)) == BLACK) { 54 setColor(sib, RED); 55 x = parentOf(x); 56 } else { 57 if (colorOf(leftOf(sib)) == BLACK) { 58 setColor(rightOf(sib), BLACK); 59 setColor(sib, RED); 60 rotateLeft(sib); 61 sib = leftOf(parentOf(x)); 62 } 63 setColor(sib, colorOf(parentOf(x))); 64 setColor(parentOf(x), BLACK); 65 setColor(leftOf(sib), BLACK); 66 rotateRight(parentOf(x)); 67 x = root; 68 } 69 } 70 } 71 72 setColor(x, BLACK); //X必定要置黑,由於X多是紅節點,也多是ROOT 73 }
上面的代碼也完整的介紹了前面介紹的刪除操做的對應邏輯,若是前面的圖理解了,相信代碼也很是容易理解。
5、總結
至此,紅黑樹的刪除就介紹完了,相對於添加來講,這個相對更復雜一些。但只要掌握了其核心思想,以及對於左右旋的操做很是熟悉,也仍是可以理解的。再次總結一下刪除的主要思想:
1. 首先要肯定當前節點是否有兩個孩子,有的話,須要找到其直接後繼,該後繼必定只有不超過一個孩子節點,轉而刪除其後繼
2. 從被刪除的節點的兒子節點開始進行刪除修復,當該兒子節點爲紅色時,直接變黑便可,若是爲黑色,則須要結合其兄弟節點的顏色一塊兒進行分析。
3. 若是兄弟節點的顏色爲紅色,則須要將兄弟節點的顏色變爲黑色,並對樹進行旋轉調整。
4. 若是兄弟節點的顏色爲黑色,則再針對其兒子節點的顏色狀況進行處理
4.1 若是兩個兒子都爲黑色,則兄弟變爲黑色,當前節點指向父親節點
4.2 若是右兒子爲黑色,則須要經過調整將右兒子變爲紅色
4.3 若是右兒子爲紅色,則經過一些節點的顏色調整,並在父節點的左旋來完成操做。
最近這兩篇文章寫了很長時間,不過經過寫這個,也算是基本上掌握了紅黑樹的主要緣由和操做,同時也以爲紅黑樹也並非那麼難以掌握,但願經過這種方式,掌握更多的經常使用算法。