最後我們來探究紅/黑樹狀結構的刪除演算法,相比插入操作,它的情況更複雜一些。因此直接考慮很容易撞到南牆,我們更需要利用轉化與化歸的思想(還記得高中數學四大思想方法吧,這裡一樣適用),通過提升變化,把紅/黑樹狀結構映射成一顆B-樹,並站在後者的角度,反過來理解前者的原理。但我們更需要關心重構作業,也就是一系列的旋轉和修複過程,並在此過程中留意他的重構次數都是$O\left( 1 \right)$層級的。
這個刪除操作還挺複雜的。。。可以說繁瑣到了讓人噁心的地步,各位做好心理準備。可以在旁邊準備個塑料袋或者盆什麼的,省的沒地方。。。
先給出一些輔助函數,方便後續使用
Position SearchIn(Position v,int e,Position& parent){//返回指向父節點指標的引用,因為後續要做左值 if (!v || (e == v->value)) return v; //遞迴基,如果直接命中或不存在則返回 parent=v; //一般情況則是先記下當前節點,然後深入一層。 return SearchIn((e < v->value ? v->left : v->right), e, parent);}//傳回值指向命中節點,parent為其父。Position Search(int target,RedBlackTree T){ return SearchIn(T, target, p);}Position GetParentOf(int e){ Search(e, fir); return p;}//由於我寫的結構體裡沒有parent指標,所以通過這個函數等效擷取。Position GetParentOf(Position T){ //得到T的父節點 Search(T->value, fir); return p;}int IsLChild(Position p){ //判斷p點是否為某個節點的左孩子 if (GetParentOf(p) != fir && GetParentOf(p)->left == p) return 1; else return 0;}Position& FromParentTo(Position p){ //返回某個節點來自父親的指標 if(GetParentOf(p) == fir) return p; //處理P是根的情況 if (GetParentOf(p) ->left ==p) // p is leftChild return GetParentOf(p)->left; else return GetParentOf(p)->right;}
紅/黑樹狀結構的刪除也類似BST刪除,自頂向下(參看前面二叉樹實現一文),如果被刪除節點X並非葉子,我們會考慮用它的直接後繼(右子樹上的最小元素)代替,這樣X頂多有一個右孩子,或者X被轉移到葉子的位置,直接刪除即可。但有一些情況需要仔細分析,因為紅/黑樹狀結構規則有其他的限制。
比如在這副圖中節點x在被刪除之後,將由它的某一個後代r來替代,這樣一來紅/黑樹狀結構的性質就未必都能繼續滿足了,驗證一下:首先紅/黑樹狀結構的根、外部節點沒有受影響,但在此局部有可能會出現兩個連續的紅色節點,而更重要的是在被刪除節點所在的路徑上,黑節點的數目 可能變化,第四條規則不一定能滿足了。另外有一大類的情況,還是非常容易處理的。就是被刪除節點x 與它的替代者r之間有一個是紅的(當然不可能全紅),如上下這兩張圖的情況。
這種情況只要把替代者r染為黑色即可,就保證第4條規則不受影響了。原因在於,從刪除操作之前的樹結構可見,在此局部都包含一條指向紅節點的虛邊,上篇說過這類虛邊對於黑高度是沒有影響的,因此在把r染黑之後,都相當於刪除了一條虛邊,因此所有外部節點的黑深度不受影響。紅色葉子的刪除還好說,問題就在於如果這葉子是黑色的,刪除之後規則4就被破壞了。解決思路就是:保證從上到下刪除期間樹葉始終是紅色的。下面詳細分析一下這一類情況。
有可能被刪除節點和替代者都是黑色的,
這種情況我們也稱之為雙黑,此時這兩個節點所屬的那條路徑而言黑長度必然會減少一個單位,從而必然違背紅/黑樹狀結構的第四條規則。而且不幸的是,前面簡明的方法,也不再有效。在給出新的方法之前,我們或需要從另一個角度來體會,問題究竟出在哪。什麼角度呢 ?當然啦,就是B樹。如果x和r都是黑色的,那麼在對應的4階B樹中,x將獨自成為一個內部節點
於是在唯一的這個關鍵碼被刪除之後,這個節點也就發生下溢。因此我們的調整演算法與其說是在紅/黑樹狀結構中修複雙黑缺陷,不如說是在B樹中修複下溢缺陷。為此我們需要考察兩個節點:首先是刪除之後,節點r的父親p;此外我們還需要在原樹中考察節點r的兄弟S。
先給出在某處刪除節點的辦法,和BST很像
Position removeAt(Position x){ Position temp; if (x->left && x->right) { temp=FindMin(x->right); x->value=temp->value; x->right=removeAt(x->right); } else{ temp=x; if(!x->left) x=x->right; else if (!x->right) x=x->left; free(temp); } return x;}//返回被刪除節點的位置
以下我們就分4種情況分別處置
第一種情況:S為黑,且至少有一個紅色孩子
以一字型為例(左),其餘的情況都與之對稱或相似。調整辦法就是做相應旋轉和重新染色(右)。染色規則是:r繼續保持黑色,而t和p都染黑,而s將繼承此前根節點p的顏色。
這裡的4棵子樹其黑高度都是一樣的,因此調整之後紅/黑樹狀結構的所有性質都恢複了。這一轉換方法並非偶然,而是有著深刻的原理,就是B樹。接下來就讓我們轉到B樹的角度,來反觀這種變換的效果。
可以看到,雙黑缺陷對應於一次下溢,所幸的是,發生下溢的這個節點擁有一個足夠富有的兄弟,可以通過旋轉消除下溢。具體來說下溢節點將從父親那借得一個關鍵碼,而父親再向那個兄弟借入一個關鍵碼以填補空缺。
經過這樣的旋轉,可以看到下溢節點得到了修複。
接下來把修複之後的B樹,還原為對應的那棵紅/黑樹狀結構即可,與直接在紅/黑樹狀結構上所做的變換是完全一致的。
新的這個關鍵碼 會依然繼承它前任的顏色,所以絕對不會在其他位置再次造成雙黑。從這個意義上講,這種情況是相對簡單的,體現在可以僅通過一次旋轉完成。換句話說至少有一個紅色孩子。那這種情況既然是簡單的,我們也很容易得知:更難的情況是沒有一個孩子為紅。那又該如何應對呢?
第二種情況:S為黑,兩個孩子都為黑。但是P是紅色。
這種情況又進而分為兩種子情況,它們的區別就在於:此時的父節點P究竟是紅還是黑,我們先討論紅色的情況。
首先將此前的紅/黑樹狀結構 轉換為對應的B樹,依然在這個位置上發生了一次下溢。此時我們並不能實施旋轉調整,原因是此時的兄弟節點s已經沒有餘糧了,自己已經處於下溢的邊緣試探了,並不足以借出任何的關鍵碼。還記得之前怎麼處理的吧,合并。從父節點中取出一個元素,並且以它作為粘合劑,將左和右兩個節點合二為一。修複的結果如下:
然後變換回對應的紅/黑樹狀結構,就可以得到在紅/黑樹狀結構中的一種可行調整方案:
現在站在紅/黑樹狀結構的角度來觀察這個過程,結果相當於r保持此前的黑色,而s由黑轉紅,同時p由紅轉黑。所以在紅/黑樹狀結構中的上述調整過程,完全等效於在B樹中某個節點通過與它的兄弟合并來消除下溢。而且這一個局部雙黑缺陷的修複,也意味著紅/黑樹狀結構的性質能夠得以在全域得到恢複,一次修複,徹底修複。
第三種情況:S為黑,兩個孩子都為黑。而且P是黑色。
同樣的站在B樹的角度來看,此時依然會發生一次下溢,而且同樣只能通過兄弟節點的合并來加以消除。
與第二種情況的不同之處在於,此時的元素p是獨自成為一個內部節點,因此當這個唯一的元素p被借出之後,此前的父節點將註定發生下溢。也就是說,在這種情況下雙黑缺陷有可能會向上傳播一層,甚至繼續上傳,直到最後的樹根。如果還採用老辦法修複,至多也就發生logn次,那問題來了,拓撲結構也會隨之變化logn次?這可不是個好訊息。
其實只要回到紅/黑樹狀結構,就可以形象地理解這個複雜的調整
需要再次強調的是:經過這樣的調整,紅/黑樹狀結構的拓撲結構沒有實質變化。也就是說 整個調整過程所執行的重構作業,不超過O(1)依然有可能落實。以下 我們只剩下最後一種情況。也就是兄弟節點s有可能不是黑色,而是紅色。
第四種情況:S為紅,孩子均為黑
參照普通BST的刪除,我們只需要轉化為之前的某種情況就行了,而不用另起爐灶。為此我們需要再次站在對應B樹的角度:
此時的p和s共同的結為一個3分支的內部節點,在此時的B樹中,只需令s和p互換顏色,而無需做任何實質的結構調整。當然在對應的紅/黑樹狀結構中,需要做一次結構調整。具體來說就是要圍繞節點p旋轉,同時翻轉s和p的顏色。
到這裡我們或許有些失望,因為問題並沒有解決。比如原先黑高度的異常依然存在。然而實際上這步轉換並非沒有意義,因為此前的矛盾焦點在於節點r的兄弟s為紅色,現在在無形中r已經擁有了一個黑的兄弟s',於是此後必然會跳出第四種情況,而轉入此前所討論的3種情況。而更好的訊息是,下面只可能轉入其中的第1或者第2種情況,而不會是第3種。因為第3種的特徵是父節點p必須是黑的,經過剛才的變換,p已經悄然變成紅色。而1,2的情況的計算複雜度更小,因為不會向上蔓延。所以經過如此調整之後,只需再做一輪遞迴,整個紅/黑樹狀結構必然會完整修複。
那麼具體的代碼實現就是:
void solveDoubleBlack(Position x){ //雙黑缺陷的修複 Position p=GetParentOf(x); //r的父親 if(!p) return; Position sibling= (x==p->left)? p->right : p->left;//r的兄弟 if (sibling -> col==black){ //兄弟為黑 Position temp=NULL; if( sibling ->left && sibling->left->col==red) temp=sibling->left; else if (sibling ->right && sibling->right->col==red) temp=sibling->right; if (temp) { //情況1:黑s有紅色孩子 Color oldCol=p->col;//備份原來的根p的顏色, FromParentTo(p) =Rotate(sibling->value,p);//做重平衡 Position b=FromParentTo(p); //然後把新子樹的左右孩子染黑 if(b->left) b->left->col=black; if(b->right) b->right->col=black; b->col=oldCol; //新樹根繼承原來的顏色 } else{ //情況2、3:黑s無紅色孩子 sibling->col=red; //s轉紅 sibling->Height--; if(p->col ==red) //情況2 p->col=black; else{ //情況3 p->Height--; //顏色保持,但是黑高度減1 solveDoubleBlack(p); }}} else{ //情況4:兄弟為紅 sibling->col=black; p->col=red; //s轉黑,p轉紅 Position t= IsLChild(sibling)?sibling->left:sibling->right; FromParentTo(p)=Rotate(sibling->value,p); } solveDoubleBlack(x);}//正式的刪除過程void Delete(int e,RedBlackTree T) { Position X = Search(e, T); //X指向被刪除節點 if(!X) printf("%d not found!",e); Position r=removeAt(X); if(GetParentOf(r) ==fir)//如果是根節點,將其染黑 r->col=black; if (r->col==red) //如果r為紅色,直接染黑就行 r->col=black; //以下情況:原來的x(現在的r,因為被刪除了嘛,然後替換了)均為黑色 solveDoubleBlack(r);}
看起來就很。。。一言難盡。這個代碼暫時還有一些小問題,但是足夠協助我們理解刪除過程了,可以暫且當成虛擬碼。 本來想著先不放上來吧,但是光看圖理解的話,似是而非,所以還是看看代碼實現吧。
現在作一總結,以下是刪除的情況分析:
每一次刪除操作 在每一高度上至多隻會花費常數時間,由此可知紅/黑樹狀結構的刪除時間複雜度不會超過$O\left( \log n \right)$。通過以上概括可以發現,紅/黑樹狀結構的刪除至多隻需做$\log n$次的重染色,以及常數次的結構調整。這也是紅/黑樹狀結構優於AVL樹的一個重要方面。還記得吧,在介紹紅/黑樹狀結構伊始就提到過,這個特性對於持久性結構的實現是至關重要的。