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