背景
紅黑樹,是一個比較復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。讓我們分析一下,整個AVL樹的性質(zhì)。AVL最明顯的特點就是,每個節(jié)點左右子樹的高度差不超過1。那么就會勢必產(chǎn)生這樣的性質(zhì):當(dāng)插入一個新的節(jié)點的時候時間復(fù)雜度是O(LogN)還有沒有辦法更快的?因此紅黑樹誕生了。
正文
先介紹一下紅黑樹的概念:
這是一種特殊的二叉搜索樹。這種二叉搜索樹將會符合如下5條性質(zhì):
1.每個節(jié)點都是黑色或者紅色的。
2.根節(jié)點是黑色的
3.每個葉子節(jié)點或者空節(jié)點(NIL)都是黑色的
4.如果一個節(jié)點是紅色的,那么他的孩子節(jié)點一定是黑色
5.從一個節(jié)點到任意一個子孫節(jié)點的所有路徑下的包含相同數(shù)目的黑色節(jié)點
這5條性質(zhì)將會確定這顆紅黑樹的所有性質(zhì)。維持紅黑樹的平衡就是通過第四和第五點兩個性質(zhì)的約束。
紅黑樹的一些有趣的性質(zhì)
1.一棵含有n個節(jié)點的紅黑樹,高度至多為
2.紅黑樹的時間復(fù)雜度為: O(lgn)
由于這本身是一個二叉搜索樹,所以樹的高度在極端情況下最多為。而到了紅黑樹,我們通過性質(zhì)4,5可以理解到如下的情況
這樣的性質(zhì)保證了紅黑樹的平衡。想想看如果我們把平衡的條件放寬一點,相比AVL樹層層調(diào)整,紅黑樹很明顯調(diào)整的次數(shù)小了2倍。因為允許左右兩側(cè)最大高度差為2倍以內(nèi)。所以相比AVL樹插入時候的O(logN),而紅黑樹的時間復(fù)雜度只有O(logN/2)
接下來,我會根據(jù)增刪查改,扣準(zhǔn)上面4,5個性質(zhì),來分別解析每個方法直接的區(qū)別。
紅黑樹的定義
同樣的,我們先定義一個紅黑樹的結(jié)構(gòu)體。
想想我們需要什么,左右節(jié)點,每個節(jié)點的鍵值對,顏色。為了方便后續(xù)的操作,還需要一個父親節(jié)點的指針。
template <class K,class V>
struct RBT::RBTreeNode{
public:
K key;
V value;
rb_color color;
RBTreeNode<K,V>* left;
RBTreeNode<K,V>* right;
RBTreeNode<K,V>* parent;
RBTreeNode( K key,
V value,
rb_color color,
RBTreeNode<K,V>* left,
RBTreeNode<K,V>* right,
RBTreeNode<K,V>* parent):key(key),value(value),
color(color),left(left),right(right),parent(parent){
}
};
紅黑樹的插入動作
紅黑樹的插入動作比較麻煩,如何看到網(wǎng)上說的情況居然分出了6種情況之多,分別處理紅黑樹的插入行為。我的老天爺啊,怎么一個插入就要分這么多種情況?第一次學(xué)習(xí)紅黑樹的哥們,一定會頭暈?zāi)X旋。實際上沒有這么可怕,插入的思想還是繼續(xù)按照AVL樹的左旋右旋進一步擴展下來的。
唯一的不同,就是為了扣緊上面五個性質(zhì)。讓紅黑樹達到自平衡。
讓我們進一步的思考一下,插入節(jié)點有什么情況。
1.插入黑色節(jié)點
當(dāng)我們插入黑色節(jié)點時候,我們會發(fā)現(xiàn),立即違反性質(zhì)五。也就是每個節(jié)點到它任意一個子孫節(jié)點的路徑上,包含的黑色節(jié)點數(shù)目都相同。那么我們想辦法要補一個黑色節(jié)點,或者通過旋轉(zhuǎn)等操作,讓其符合性質(zhì)四,五。這種方案比較麻煩,看看插入紅色節(jié)點。2.插入紅色節(jié)點
如果插入紅色節(jié)點,可以發(fā)現(xiàn),此時可能違反性質(zhì)四,不違反性質(zhì)5.這樣我們能少考慮這上面情況,處理路徑上的黑色節(jié)點數(shù)目比較困難,因此,我們將每一個新的節(jié)點先變成紅色,再插入,就能盡可能避免更多的變化。但是記住我們的根節(jié)點是黑色的,所以最后要染黑。
當(dāng)然,假如我們的父親節(jié)點本身就是紅色節(jié)點怎么辦?這樣就違反性質(zhì)4,紅色節(jié)點的孩子節(jié)點必定是黑色節(jié)點。但是相比違反性質(zhì)5,我們要做的工作會少很多。
讓我們來寫寫,插入節(jié)點全部染成紅色的情況。
template <class K,class V>
RBTreeNode<K,V>* RBT::insert(K key,V value) {
if(!root){
root = new RBTreeNode<K,V>(NULL,NULL,black,NULL,NULL,NULL);
return root;
}
RBTreeNode<K,V> *rb_node = root;
RBTreeNode<K,V> *parent = NULL;
//不允許去修改,學(xué)習(xí)binder
do{
parent = rb_node;
if(key == rb_node->key){
return rb_node;
} else if(key > rb_node->key){
rb_node = rb_node->right;
} else{
rb_node = rb_node->left;
}
}while (rb_node);
//知道找到對應(yīng)的父親節(jié)點,添加進去
RBTreeNode<K,V> *new_node = new RBTreeNode<K,V>(key,value,red,NULL,NULL,parent);
//父親節(jié)點
if(parent->key > key){
parent->left = new_node;
} else{
parent->right = new_node;
}
//父親節(jié)點也添加好之后,解決雙紅問題
solveDoubleRed(new_node);
count++;
return new_node;
}
思路很簡單,和AVL樹一模一樣,首先先找出應(yīng)該在哪個父親節(jié)點下面添加節(jié)點,并且添加下去。最后記得,由于我們這里多了parent節(jié)點的屬性,我們需要根據(jù)key的大小,添加到對應(yīng)的左樹還是右樹。
最后一旦發(fā)現(xiàn)父親節(jié)點是紅色,我們必須處理一下,雙紅現(xiàn)象。這個處理雙紅就是整個插入之后使得紅黑樹平衡的。
我們深入思考一下插入節(jié)點是紅色的,在平衡的過程中會遇到什么阻礙。
最好的結(jié)果把這個多余的紅色節(jié)點平衡到以另一端,這樣這一側(cè)紅色就能避免雙紅。
那么我們遇到第一種情況:
此時父親節(jié)點為黑色,直接加進去,最后染黑該節(jié)點,沒有任何問題,沒有違反任何性質(zhì)。
第二種情況:
遇到這種情況,怎么辦?為了保證性質(zhì)5.我們試試把本節(jié)點以外的節(jié)點的一些節(jié)點染黑看看,最后為了性質(zhì)3,再把葉子節(jié)點變黑,能不能達到平衡。
最直接的做法,試試把父親染黑,保證性質(zhì)4.
不好這樣又破壞了性質(zhì)5,亡羊補牢一下,我們把爺爺節(jié)點染成紅色!
好像整個演變都對了。那么我們可以探索出變化時候的其中一個在旋轉(zhuǎn)要點,變化顏色請成對的變化。這樣能保證我們在旋轉(zhuǎn)的時候維持紅黑點的數(shù)量保持為原來的數(shù)目。
其實想想也很簡單,只是變化一個節(jié)點的話,那么勢必會打破原來已經(jīng)平衡的紅黑樹。那么我們這一次,為了扣緊5個性質(zhì),一口氣變化紅黑樹上的父親和爺爺節(jié)點,讓變化過程盡可能的維持平衡。
插入的第二情況的解決辦法:
如果叔叔是黑色的,且新插入的節(jié)點同于生長方向,父親染黑,爺爺染紅,接著左右旋旋轉(zhuǎn)
情況三:
這樣這種情況就分出一個分支了,當(dāng)插入的節(jié)點是右孩子的時候,一次右旋是不可能維持到達上圖的最后一個狀態(tài)。所以只要我們在出這些步驟之前,對著福清節(jié)點左旋達到上圖的狀態(tài)一即可。
這樣就是叔叔為黑色,并且加在左樹的狀態(tài)。同理當(dāng)我們把節(jié)點加到右邊,步驟不變,只是旋轉(zhuǎn)的方向和加在左樹的變化相反即可。
如果叔叔是黑色的,且不同于生長方向,父親先左右旋轉(zhuǎn),染黑此時的父親,爺爺染紅,接著左右旋旋轉(zhuǎn)
這樣就是5種情況了。
上面的情況有個共同點,那就是叔叔是黑色的,并且父親是紅色。當(dāng)叔叔節(jié)點變成紅色呢?這個又怎么分析的。
情況六
沒想到當(dāng)叔叔是紅色的時候,我們把父親染黑,把爺爺染紅,叔叔染黑,就過右旋就能完成了平衡了。
但是事情是這么簡單嗎?別忘了,我們這個時候是在對三個節(jié)點變化了顏色,并沒有成對的變色。雖然在這個樹的高度只有2的情況下,剛好能夠符合情況,但是高度再高一層,紅黑樹會因為染色不對稱導(dǎo)致,整個樹的平衡被破壞。
因此為了保證整個紅黑樹的自平衡,我們選擇把指針移動到爺爺節(jié)點,讓爺爺節(jié)點作為新的處理對象,看看上面的分支是否會出現(xiàn)自平衡被破壞。
如果叔叔是紅色的,把父親染黑,爺爺染紅,叔叔也要染黑,達到上面能夠旋轉(zhuǎn)到位的情況,由于染色不均衡,我們把指針指向爺爺,讓爺爺去上層平衡。
這樣6種情況全部分析完。
為了操作足夠方便,先提供尋找兄弟節(jié)點,父親節(jié)點,以及染色的方法
template <class K,class V>
rb_color RBT::getColor(RBTreeNode<K,V> *node){
return node?node->color : black;
}
template <class K,class V>
RBTreeNode<K,V>* RBT::setColor(RBTreeNode<K,V> *node,rb_color color){
if(node){
node->color = color;
}
}
template <class K,class V>
RBTreeNode<K,V>* RBT::left(RBTreeNode<K,V> *node){
return node ? node->left : NULL;
}
template <class K,class V>
RBTreeNode<K,V>* RBT::right(RBTreeNode<K,V> *node){
return node ? node->right : NULL;
}
template <class K,class V>
RBTreeNode<K,V>* RBT::parent(RBTreeNode<K,V> *node){
return node ? node->parent : NULL;
}
template <class K,class V>
RBTreeNode<K,V>* RBT::brother(RBTreeNode<K,V> *node){
if(!node||!node->parent) {
return NULL;
}
return left(parent(node)) == node ? right(parent(node)) : left(parent(node)) ;
}
完成之后,讓我根據(jù)上面分析嘗試著實現(xiàn)代碼。
void solveDoubleRed(TreeNode *pNode){
//情況1:父親是黑色節(jié)點不需要調(diào)整直接跳出循環(huán)
while(pNode->parent && pNode->parent->color == red){
//情況2:叔叔是紅色,則把叔叔和父親染成黑色,爺爺染成紅色指針回溯到爺爺,交給爺爺去處理
if(getColor(brother(parent(pNode))) == red){
setColor(parent(pNode),black);
setColor(brother(parent(pNode)),black);
setColor(parent(parent(pNode)),red);
pNode = parent(parent(pNode));
} else{
//情況3:叔叔是黑色
//如果叔叔是黑色的,我們把父親染成黑色,把爺爺染成紅色,
if(left(parent(parent(pNode))) == parent(pNode)){
//3.1.此時當(dāng)前節(jié)點是左子樹的父親右節(jié)點,與原來生長方向不一致
if(right(parent(pNode)) == pNode){
//先把父親左旋一次,保證原來的方向
pNode = parent(pNode);
L_Rotation(pNode);
}
//3.2把這個子樹的紅色節(jié)點,挪動到叔叔的那顆樹上.也就是父親和舅舅變黑,爺爺變成紅色
//再右旋轉(zhuǎn)
//右旋一次爺爺
setColor(parent(pNode),black);
setColor(parent(parent(pNode)),red);
R_Rotation(parent(parent(pNode)));
} else{
//3.1.此時當(dāng)前節(jié)點是右子樹的父親左節(jié)點,與原來生長方向不一致
if(left(parent(pNode)) == pNode){
//先把父親右旋一次,保證原來的方向
pNode = parent(pNode);
R_Rotation(pNode);
}
//3.2把這個子樹的紅色節(jié)點,挪動到叔叔的那顆樹上.也就是父親和舅舅變黑,爺爺變成紅色
//再左旋轉(zhuǎn)爺爺
setColor(parent(pNode),black);
setColor(parent(parent(pNode)),red);
L_Rotation(parent(parent(pNode)));
}
}
}
root->color = black;
}
弄懂了,就很簡單吧。這里面有著左旋和右旋操作,這里面的實現(xiàn)和AVL樹極其相似,實際上就是因為RBTreeNode中多了parent屬性,所以我們要對parent屬性進行鏈接。
思路還是沿著AVL樹。這就是為什么我要先分析AVL樹。
template <class K,class V>
RBTreeNode<K,V> *RBT::R_Roation(RBTreeNode<K,V> *node){
//右旋,把左邊的節(jié)點作為父親
RBTreeNode<K,V> *result_node = node->left;
//原來右邊的節(jié)點到左邊去
node->left = result_node->right;
result_node->right = node;
return result_node;
}
但是這樣就萬事大吉了嗎?實際上,我們在這里面對幾個點做了變動,result_node,node_left,node.這些點的parent還是指向原來的地方呢?還沒有做更替。
因此右旋完整的代碼應(yīng)該如下:
template <class K,class V>
RBTreeNode<K,V> *RBT::R_Roation(RBTreeNode<K,V> *node){
//右旋,把左邊的節(jié)點作為父親
RBTreeNode<K,V> *result_node = node->left;
//左邊
node->left = result_node->right;
result_node->right = node;
//記住最后要處理這幾個節(jié)點的parent
//此時parent 可能為空,此時為根
//記住處理一下node本身的parent
if(!node->parent){
root = result_node;
} else if(node->parent->left = node){
node->parent->left = result_node;
} else{
node->parent->right = result_node;
}
result_node->parent = node->parent;
if(node->left){
node->left->parent = result_node;
}
node->parent = result_node;
return result_node;
}
node,result_node,node_left的父親全部都做了處理,同理左旋則是下面的代碼
template <class K,class V>
RBTreeNode<K,V> *RBT::L_Roation(RBTreeNode<K,V> *node){
//右旋,把左邊的節(jié)點作為父親
RBTreeNode<K,V> *result_node = node->right;
//左邊
node->right = result_node->left;
result_node->left = node;
//記住最后要處理這幾個節(jié)點的parent
//此時parent 可能為空,此時為根
//記住處理一下node本身的parent
if(!node->parent){
root = result_node;
} else if(node->parent->left = node){
node->parent->left = result_node;
} else{
node->parent->right = result_node;
}
result_node->parent = node->parent;
if(node->right){
node->right->parent = result_node;
}
node->parent = result_node;
return result_node;
}
這樣就完成了插入的動作。
讓我們試試測試代碼吧。
RBT<int,int> *map = new RBT<int,int>();
map->insert(3,3);
map->insert(2,2);
map->insert(1,1);
map->insert(4,4);
map->insert(5,5);
map->insert(-5,-5);
map->insert(-15,-15);
map->insert(-10,-10);
map->insert(6,6);
map->insert(7,7);
//
// map->remove(2);
// map->remove(-5);
//map->insert(3,11);
map->levelTravel(visit_rb);
測試結(jié)果:
我們分解一下步驟,看看這個過程是否正確。
根據(jù)先序遍歷,輸出的打印應(yīng)該為2(黑),-5( 紅),4(紅),-15(黑),1(黑),3(黑),6(黑),5(紅),7(紅)
結(jié)果正確。
紅黑樹的插入檢驗完畢。讓我們來討論討論紅黑樹的刪除。
紅黑樹的刪除
紅黑樹的刪除比起紅黑樹插入還要復(fù)雜。實際上,只要我們小心的分析每個步驟,也能盲敲出來。
我們繼續(xù)延續(xù)AVL樹的思想與步驟。
我們刪除節(jié)點還是圍繞三種基本情況來討論。
- 1.當(dāng)刪除的節(jié)點沒有任何孩子
我們直接刪除該節(jié)點 - 2.當(dāng)刪除的節(jié)點只有一個孩子
我們會拿他的左右其中一個節(jié)點來代替當(dāng)前節(jié)點 - 3.當(dāng)刪除的節(jié)點兩側(cè)都有孩子
我們會刪除該節(jié)點,并且找到他的后繼來代替。
換句話說,我會延續(xù)之前的思路,找到后續(xù)節(jié)點代替到當(dāng)前要刪除的節(jié)點,最后再刪掉這個重復(fù)的后繼節(jié)點
到這里都和二叉搜索樹極其相似。但是不要忽略了我們5個性質(zhì)。當(dāng)我們刪除的時候,為了保持紅黑樹自平衡,可以預(yù)測到的是有如下兩條規(guī)則:
- 1.刪除紅色節(jié)點,不破壞性質(zhì)5,不影響平衡。
- 2.刪除黑色節(jié)點必定破壞性質(zhì)5,導(dǎo)致當(dāng)前紅黑樹的被破壞
我們結(jié)合著5條規(guī)則看看,究竟該怎么刪除??纯磩h除需要遵守什么規(guī)則才能保持紅黑樹的自平衡。
我們依照著插入思路倒推一下。我們想要保持紅黑樹的這一側(cè)被刪除的節(jié)點的平衡,大致思路是什么?
首先,我們刪除的節(jié)點的時候。如果直接按照搜索二叉樹的思路直接刪除,但是執(zhí)行刪除之前,我們一定會遇到刪除的節(jié)點是黑色節(jié)點或紅色節(jié)點情況。
根據(jù)上面的兩條規(guī)則,假如移除一個紅色節(jié)點不會破壞性質(zhì)4,性質(zhì)5.沒有問題,我們可以直接刪除。但是一旦遇到黑色節(jié)點一定破壞性質(zhì)5.那么我們怎么辦呢?
我們能夠想到的一個簡單的辦法:就是從兄弟那邊拿一個紅色節(jié)點過來,再染黑這個節(jié)點,給到刪除的那一側(cè)。就能以最小的代價保持紅黑樹的平衡了。
很好,這個思路就是最為關(guān)鍵的核心的。那么我們實際推敲一下,在這個過程中我們會遇到什么情況吧。
情況一
直接刪除紅色節(jié)點不影響平衡。
接下來我們來考慮移除黑色節(jié)點時候該怎么處理。
情況二
此時我們要移除2,勢必造成紅黑樹平衡被破壞。雖然,我們一眼能看出結(jié)果這個樹該怎么平衡,但是我們分解步驟看看其中有什么規(guī)律。
我們學(xué)習(xí)紅黑樹插入原理嘗試著成對的處理紅黑節(jié)點,把父親節(jié)點染紅和兄弟節(jié)點染黑再左旋看看結(jié)果。
依照這個這樣下去似乎就平衡了?我們探索除了,假如兄弟節(jié)點是黑色,就把遠侄子染黑就好了嗎?別忘了我們的染色是為了讓去除的一側(cè)憑空多出一個黑色節(jié)點,來保證紅黑樹的平衡。此時我們的紅黑樹恰好只有一層,我們只需要稍微旋轉(zhuǎn)一下就能達到平衡。所以此時是一種特殊情況。
這種情況應(yīng)該是特殊情況。我們再看看其他的情況
在這個時候我們嘗試學(xué)習(xí)上面的辦法,先把g染黑進行左旋,會發(fā)現(xiàn)根本不平衡。我們看看下面的變化。
但是思路已經(jīng)開啟了,我們就要多出一個紅色點,轉(zhuǎn)到刪除的那一側(cè)。最后再把這個紅色點變成黑色。
也就是說,我們試著把5染成紅色,2染成黑色,8染成黑色,左旋即可。這樣我們可以總結(jié)出一個旋轉(zhuǎn)平衡的操作:
當(dāng)兄弟節(jié)點是黑色,且遠侄子是紅色的時候。我們把兄弟染成父親的顏色,再把父親染黑,遠侄子染黑,進行左/右旋轉(zhuǎn)父親即可達到平衡。
實際上,這么做的目的很簡單,讓父親變成黑的,補償被刪除的那一端,這樣就能補充那一側(cè)的節(jié)點,同時遠侄子從紅色染黑了,保證補償?shù)囊粋?cè)多出一個黑色節(jié)點。而把兄弟染成父親的顏色是為了保持這段子樹的平衡。
這個情況二十分重要。刪除的情況十分復(fù)雜,但是我們?nèi)绻馨堰@些情況全部轉(zhuǎn)化為當(dāng)前這個情況。我們就能保證紅黑樹每一處都到了平衡。
情況三
此時當(dāng)我們的兄弟節(jié)點是黑色,且遠侄子為紅色的時候是這樣操作。那假如兄弟節(jié)點是黑色,近侄子是紅色,遠侄子是黑色。怎么辦?
下面的情況是某一個紅黑樹的一部分
在這個時候,我們想辦法變成上面的,先試試把遠侄子染成紅色,為了保持這邊的平衡,也要把父親染紅
和情況二相似,但是近侄子和遠侄子的顏色相反過來。我們順著插入操作順著推下去,我們應(yīng)該要轉(zhuǎn)變成情況二那種狀況,再去平衡整個紅黑樹。
關(guān)鍵是我們該怎么在不影響樹的平衡的情況下,轉(zhuǎn)化為情況二
但是這么做有個問題,萬一g此時的孩子節(jié)點是一個紅色節(jié)點,就變得我們不得不去解決雙紅現(xiàn)象。這樣反而更加麻煩。變量太多了,反而不好維持平衡。
所以上面的變化是不推薦嘗試的。
我們試試這樣的方式。我們?nèi)竞诮蹲?,染紅兄弟,進行左旋。一樣能夠辦到上面的情況
此時就是我們想要的情況,我們要刪除下方的c節(jié)點,此時兄弟是黑色,遠侄子是紅色,同時近侄子是黑色。這樣我們就進入到了情況二。
我們再把兄弟染成父親的顏色,父親再染黑,遠侄子再染黑,右旋整個樹
我們數(shù)數(shù)看黑色節(jié)點數(shù)目,雖然這是紅黑樹的一部分節(jié)點。但是我們可以通過這種手段來維持這部分樹的,黑色節(jié)點數(shù)目的不變。
這樣我們又探索出了一個新的平衡條件
如果兄弟節(jié)點是黑色,遠侄子是黑色,近侄子是紅色的,我們把兄弟染紅,近侄子染黑,再左右兄弟旋轉(zhuǎn),就能達到情況二。能通過情況二把紅黑樹平衡下來
情況四
當(dāng)我們的兄弟節(jié)點是黑色,遠侄子是紅色的,近侄子是黑色的(我們期望的能夠一步達到的平衡條件,因為多出一個紅色節(jié)點,能夠通過染黑遠侄子,旋轉(zhuǎn)補償刪除的一側(cè))。以及兄弟節(jié)點是黑色,遠侄子是黑色的,近侄子是紅色的。
那么我們來考慮一下,當(dāng)兄弟節(jié)點是黑色,下面兩個侄子都是黑色的時候怎么辦?我們沒有默認的紅色節(jié)點啊,沒辦法給刪除的那一側(cè)補償啊。
下面是紅黑樹的一部分:
我們刪除a的話。此時怎么辦?都是黑色。沒辦法補償右側(cè)啊。我們只能學(xué)習(xí)插入的情況六。先染一個紅色的節(jié)點出來,把希望寄托與上層。
但是我們選擇怎么染顏色呢?還記得我們的情況二這種一步達到的平衡的狀況,既然沒有,我們就創(chuàng)造一個出來。
我們本來就要把指針移動到a(父親)出,從上層尋找機會。那么此時的b就是相對與上層的遠侄子了。那么我們把此時c的兄弟節(jié)點,b染紅即可。這樣我們就創(chuàng)造了一個遠侄子是紅色的情況。
這樣我們又解決了一個新的情況。
如果兄弟是黑色的,且兩個侄子(兄弟兩個孩子)也是黑色的,則把兄弟染成紅色,把指針指向父親。此時就可以變化為接近情況二的狀態(tài),指針指向父親,讓父親從上層找機會跳針。
情況五
我們一直在探討兄弟是黑色的,假如兄弟是紅色又怎么辦。
下面是紅黑樹的某一部分:
感覺此時的情況很好解決。因為此時兄弟本來就是紅色的,也就是說本來就又一個紅色節(jié)點提供給我們。如果能夠搬到把這個節(jié)點補償?shù)搅硪粋?cè)就完成。
但實際上我們思考一下就明白了,為什么我們上面要以遠侄子為紅色而不是兄弟而紅色呢?實際上很簡單,我們紅兄弟染黑,通過左旋和右旋,此時兄弟會成為這個子樹的根,會導(dǎo)致兩側(cè)都增加黑色節(jié)點,這樣還是不符合我們的邏輯。因此此時我們只能嘗試著把這種情況往情況二,三,四變化。
因此我們嘗試著把兄弟染黑,父親染紅
我們再對父親c進行右轉(zhuǎn):
這樣我們不斷的經(jīng)歷著遇到兄弟是紅色的時候,不斷染黑兄弟,父親染紅,在旋轉(zhuǎn),一定能遇到兄弟是黑色的情況。這樣就回到我們的情況二三四了。
這樣我們探索出最后一種情況。
當(dāng)兄弟是紅色的時候,染黑兄弟,染紅父親,左右旋父親。
一直在刪除右側(cè),實際上我們還有左側(cè)情況考慮。
也就是說,刪除一共有9種情況考慮。這樣我們就把所有的請考慮下來了,接下來讓我試試盲敲一遍。
在這之前,我要提供幾個函數(shù),方便我后續(xù)工作:
尋找后繼
RBTreeNode* succeed(){
//找后繼,找右邊的最小
RBTreeNode *node = right;
if(!node){
while (node->left){
node = node->left;
}
return node;
} else{
//當(dāng)右側(cè)沒有的時候
node = this;
//當(dāng)右側(cè)沒有的時候,不斷向上找,找到此時是父親的左孩子就是后繼
while (node->parent && node->parent->right == node){
node = node->parent;
}
return node->parent;
}
}
RBTreeNode* findTree(K key){
RBTreeNode *node = root;
while (node){
if(key < node->key){
node = node->left;
} else if(key > node->key){
node = node->right;
} else {
return node;
}
}
}
接下來我們來看看正式的刪除實現(xiàn):
bool remove(K key){
RBTreeNode *current = findTree(key);
if(!current){
return false;
}
//找到節(jié)點之后,判斷當(dāng)前節(jié)點的孩子節(jié)點是兩個還是一個還是沒有
if(current->left && current->right){
//如果有兩個節(jié)點,則取后繼來代替當(dāng)前
RBTreeNode *succeed = current->succeed();
//此時已經(jīng)替換過來了,并且做替換
//此時我們要把原來節(jié)點的數(shù)據(jù)更改過來,但是節(jié)點結(jié)構(gòu)不變
current->key = succeed->key;
current->value = succeed->value;
//此時,我們要調(diào)整的對象應(yīng)該是后繼
current= succeed;
}
RBTreeNode* replace = current->left? current->left : current->right;
//此時我們判斷是左還是右把左子樹還是右子樹放上來
//延續(xù)之前的思想
if(replace){
//斷開原來所有的數(shù)據(jù),把子孩子代替上來
//思路是把當(dāng)前parent的節(jié)點,連上replace
if(!current->parent){
//說明當(dāng)前已經(jīng)是根部
root = replace;
} else if(current->parent->left == current){
//說明此時左邊節(jié)點,我們要把數(shù)據(jù)代替到父親的左節(jié)點
current->parent->left = replace;
} else{
current->parent->right = replace;
}
//替換掉節(jié)點
replace->parent = current->parent;
if(current->color == black){
//處理代替的節(jié)點
solveLostblack(replace);
}
delete(current);
} else if(current->parent == NULL){
//此時已經(jīng)是根部了
delete(root);
root = NULL;
} else{
if(current->color == black){
solveLostblack(current);
}
//把current的parent的孩子信息都清空
if(current->parent->left == current){
current->parent->left = NULL;
} else {
current->parent->right = NULL;
}
//此時是葉子節(jié)點
delete(current);
}
count--;
return true;
}
關(guān)鍵是怎么解決刪除黑色節(jié)點問題。
void solveLostblack(RBTreeNode *node){
//此時進入情況一
//當(dāng)節(jié)點刪除的節(jié)點是紅色則不用管
while(node != root&& node->color == black){
//此時判斷當(dāng)前是左樹還是右樹
if(node->parent->left == node){
//此時進入情況五,兄弟節(jié)點是紅色
RBTreeNode *sib = brother(node);
if(getColor(brother(node)) == red){
//兄弟染黑,父親染紅,刪除了左樹,補償左樹,左旋父親
setColor(brother(node),black);
setColor(parent(node),red);
L_Roation(parent(node));
sib = brother(node);
}
//此時進入情況3/4
//情況四
//兄弟是黑,兩個侄子也是黑
if(getColor(sib)==black
&& getColor(left(sib)) == black
&& getColor(right(sib)) == black){
//兄弟染紅,指針移動到父親,創(chuàng)造一個紅色遠侄子
setColor(sib,red);
node = parent(node);
} else {
//如果兄弟是黑,遠侄子是黑
//此時近侄子是左,遠侄子是右
if( getColor(right(sib)) == black){
//還是想辦法創(chuàng)造一個紅色的遠侄子
//兄弟變紅
setColor(sib,red);
//近侄子變黑
setColor(left(sib),black);
//此時遠侄子在右邊,我們需要右旋
R_Roation(sib);
sib = brother(node);
}
//此時兄弟是黑,遠侄子是紅色
//把兄弟染成父親的顏色,父親染黑,遠侄子染黑,左旋
setColor(sib,getColor(parent(node)));
setColor(parent(node),black);
setColor(right(sib),black);
L_Roation(parent(node));
//此時已經(jīng)沒有必要在調(diào)整了,已經(jīng)成功了
node = root;
}
} else{
RBTreeNode *sib = brother(node);
//此時進入情況五,兄弟節(jié)點是紅色
if(getColor(sib) == red){
//兄弟染黑,父親染紅,刪除了左樹,補償右樹,右旋父親
setColor(sib,black);
setColor(parent(node),red);
R_Roation(parent(node));
sib = brother(node);
}
//此時進入情況3/4
//情況四
//兄弟是黑,兩個侄子也是黑
if(getColor(sib)==black
&& getColor(left(sib)) == black
&& getColor(right(sib)) == black){
//兄弟染紅,指針移動到父親,創(chuàng)造一個紅色遠侄子
setColor(sib,red);
node = parent(node);
} else {
//如果兄弟是黑,遠侄子是黑
//此時近侄子是右,遠侄子是左
if( getColor(left(sib)) == black){
//還是想辦法創(chuàng)造一個紅色的遠侄子
//兄弟變紅
setColor(sib,red);
//近侄子變黑
setColor(right(sib),black);
//此時遠侄子在右邊,我們需要右旋
L_Roation(sib);
sib = brother(node);
}
//此時兄弟是黑,遠侄子是紅色
//把兄弟染成父親的顏色,父親染黑,遠侄子染黑,左旋
setColor(sib,getColor(parent(node)));
setColor(parent(node),black);
setColor(left(sib),black);
R_Roation(parent(node));
//此時已經(jīng)沒有必要在調(diào)整了,已經(jīng)成功了
node = root;
}
}
}
node->color = black;
}
按照思路已經(jīng)完成整個思想,我來測試看看究竟對不對
RBT<int,int> *map = new RBT<int,int>();
map->insert(3,3);
map->insert(2,2);
map->insert(1,1);
map->insert(4,4);
map->insert(5,5);
map->insert(-5,-5);
map->insert(-15,-15);
map->insert(-10,-10);
map->insert(6,6);
map->insert(7,7);
//
map->remove(2);
map->remove(-5);
map->levelTravel(visit_rb);
我們來試試分解步驟進行解析
根據(jù)前序便利,結(jié)果是3,-10,6,-15,1,4,7,5
結(jié)果正確。
總結(jié)
紅黑樹是我們初級程序員能夠接觸到幾乎最復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。我也花了好長時間的學(xué)習(xí),推導(dǎo),盲敲以及修改bug。
根據(jù)我的盲敲的心得,紅黑樹的插入,刪除有這么一個小訣竅。
插入看叔叔,刪除看兄弟。插入避免雙紅,刪除處理丟黑。
記住插入根本,當(dāng)叔為黑,父染黑,爺染紅,根據(jù)情況左右旋
記住刪除根本,當(dāng)兄為黑,遠侄子為紅,就把兄弟染成父親色,父親遠侄子染黑,根據(jù)情況左右旋。
遇到生長不如意,反向旋轉(zhuǎn)父或兄,回到根本去平衡。
倘若遇到,插入叔為紅,父染黑,爺染紅;
刪除侄子都為黑,兄染紅,回溯上層找機會。
刪除遇到兄弟為紅,染黑兄,染紅父,根據(jù)情況左右旋。
實際上插入和刪除的操作,從根本就是抓住5個性質(zhì),所以實際上還是有很大的相似性。只要記住一點,插入避免雙紅,我們就要看叔叔那邊的情況,能不能處理雙紅,畢竟也要即使處理完這一側(cè)的雙紅,也要避免另一側(cè)的雙紅。
刪除處理丟黑,能不能處理丟黑,就要看看兄弟是否是黑以及遠侄子是否是紅。遠侄子是否為紅代表著是否能通過不改變這一側(cè)的黑色節(jié)點數(shù),為刪除的一側(cè)添加黑色節(jié)點,而兄弟節(jié)點是否是黑色決定著其侄子究竟有沒有紅。如果兄弟為紅色,我們必須進行旋轉(zhuǎn),來達到我們兄弟為黑色情況,這樣我們就能避免雙紅的出現(xiàn),同時處理遠侄子為紅的條件。
說了這么多,本來想結(jié)合binder的紅黑樹一起來探討,但是篇幅有限,我就不再這里贅述了。我本來以為我沒辦法盲敲出紅黑樹的,畢竟我當(dāng)年第一次接觸的時候,腦子亂的。但是仔細分析了6種插入情況,9種刪除情況,發(fā)現(xiàn)自己也行的,沒有想象的這么難。
這次紅黑樹的盲敲讓我明白了,很多看起來困難的事情,只要自己一步一腳印的去做,或許能達到意想不到的效果呢。