Android重學(xué)系列 紅黑樹

背景

紅黑樹,是一個比較復(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é)點的紅黑樹,高度至多為2log_2{N+1}
2.紅黑樹的時間復(fù)雜度為: O(lgn)

由于這本身是一個二叉搜索樹,所以樹的高度在極端情況下最多為O(N)。而到了紅黑樹,我們通過性質(zhì)4,5可以理解到如下的情況

image.png

這樣的性質(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è)紅色就能避免雙紅。

那么我們遇到第一種情況:

情況一.png

此時父親節(jié)點為黑色,直接加進去,最后染黑該節(jié)點,沒有任何問題,沒有違反任何性質(zhì)。

第二種情況:

情況二.png

遇到這種情況,怎么辦?為了保證性質(zhì)5.我們試試把本節(jié)點以外的節(jié)點的一些節(jié)點染黑看看,最后為了性質(zhì)3,再把葉子節(jié)點變黑,能不能達到平衡。

最直接的做法,試試把父親染黑,保證性質(zhì)4.


情況二第一次變化.png

不好這樣又破壞了性質(zhì)5,亡羊補牢一下,我們把爺爺節(jié)點染成紅色!


情況二第二次變化.png

好像整個演變都對了。那么我們可以探索出變化時候的其中一個在旋轉(zhuǎn)要點,變化顏色請成對的變化。這樣能保證我們在旋轉(zhuǎn)的時候維持紅黑點的數(shù)量保持為原來的數(shù)目。

其實想想也很簡單,只是變化一個節(jié)點的話,那么勢必會打破原來已經(jīng)平衡的紅黑樹。那么我們這一次,為了扣緊5個性質(zhì),一口氣變化紅黑樹上的父親和爺爺節(jié)點,讓變化過程盡可能的維持平衡。

插入的第二情況的解決辦法:

如果叔叔是黑色的,且新插入的節(jié)點同于生長方向,父親染黑,爺爺染紅,接著左右旋旋轉(zhuǎn)

情況三:

情況三.png

這樣這種情況就分出一個分支了,當(dāng)插入的節(jié)點是右孩子的時候,一次右旋是不可能維持到達上圖的最后一個狀態(tài)。所以只要我們在出這些步驟之前,對著福清節(jié)點左旋達到上圖的狀態(tài)一即可。

這樣就是叔叔為黑色,并且加在左樹的狀態(tài)。同理當(dāng)我們把節(jié)點加到右邊,步驟不變,只是旋轉(zhuǎn)的方向和加在左樹的變化相反即可。

如果叔叔是黑色的,且不同于生長方向,父親先左右旋轉(zhuǎn),染黑此時的父親,爺爺染紅,接著左右旋旋轉(zhuǎn)

這樣就是5種情況了。

上面的情況有個共同點,那就是叔叔是黑色的,并且父親是紅色。當(dāng)叔叔節(jié)點變成紅色呢?這個又怎么分析的。

情況六

image.png

沒想到當(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é)果:


測試結(jié)果.png

我們分解一下步驟,看看這個過程是否正確。

紅黑樹添加節(jié)點步驟1.png
紅黑樹添加結(jié)點分解步驟2.png

根據(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)鍵的核心的。那么我們實際推敲一下,在這個過程中我們會遇到什么情況吧。

情況一

image.png

直接刪除紅色節(jié)點不影響平衡。

接下來我們來考慮移除黑色節(jié)點時候該怎么處理。

情況二

情況二.png

此時我們要移除2,勢必造成紅黑樹平衡被破壞。雖然,我們一眼能看出結(jié)果這個樹該怎么平衡,但是我們分解步驟看看其中有什么規(guī)律。

我們學(xué)習(xí)紅黑樹插入原理嘗試著成對的處理紅黑節(jié)點,把父親節(jié)點染紅和兄弟節(jié)點染黑再左旋看看結(jié)果。

情況二變化一.png

依照這個這樣下去似乎就平衡了?我們探索除了,假如兄弟節(jié)點是黑色,就把遠侄子染黑就好了嗎?別忘了我們的染色是為了讓去除的一側(cè)憑空多出一個黑色節(jié)點,來保證紅黑樹的平衡。此時我們的紅黑樹恰好只有一層,我們只需要稍微旋轉(zhuǎn)一下就能達到平衡。所以此時是一種特殊情況。

這種情況應(yīng)該是特殊情況。我們再看看其他的情況


情況二變化二.png

在這個時候我們嘗試學(xué)習(xí)上面的辦法,先把g染黑進行左旋,會發(fā)現(xiàn)根本不平衡。我們看看下面的變化。

但是思路已經(jīng)開啟了,我們就要多出一個紅色點,轉(zhuǎn)到刪除的那一側(cè)。最后再把這個紅色點變成黑色。

情況二變化三.png

也就是說,我們試著把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é)點是黑色,近侄子是紅色,遠侄子是黑色。怎么辦?

下面的情況是某一個紅黑樹的一部分


情況三(紅黑樹的一部分).png

在這個時候,我們想辦法變成上面的,先試試把遠侄子染成紅色,為了保持這邊的平衡,也要把父親染紅

和情況二相似,但是近侄子和遠侄子的顏色相反過來。我們順著插入操作順著推下去,我們應(yīng)該要轉(zhuǎn)變成情況二那種狀況,再去平衡整個紅黑樹。

關(guān)鍵是我們該怎么在不影響樹的平衡的情況下,轉(zhuǎn)化為情況二

情況三變化一.png

但是這么做有個問題,萬一g此時的孩子節(jié)點是一個紅色節(jié)點,就變得我們不得不去解決雙紅現(xiàn)象。這樣反而更加麻煩。變量太多了,反而不好維持平衡。

所以上面的變化是不推薦嘗試的。

我們試試這樣的方式。我們?nèi)竞诮蹲?,染紅兄弟,進行左旋。一樣能夠辦到上面的情況

情況三變化二.png

此時就是我們想要的情況,我們要刪除下方的c節(jié)點,此時兄弟是黑色,遠侄子是紅色,同時近侄子是黑色。這樣我們就進入到了情況二。

我們再把兄弟染成父親的顏色,父親再染黑,遠侄子再染黑,右旋整個樹


情況三變化到情況二.png

我們數(shù)數(shù)看黑色節(jié)點數(shù)目,雖然這是紅黑樹的一部分節(jié)點。但是我們可以通過這種手段來維持這部分樹的,黑色節(jié)點數(shù)目的不變。

這樣我們又探索出了一個新的平衡條件

如果兄弟節(jié)點是黑色,遠侄子是黑色,近侄子是紅色的,我們把兄弟染紅,近侄子染黑,再左右兄弟旋轉(zhuǎn),就能達到情況二。能通過情況二把紅黑樹平衡下來

情況四

當(dāng)我們的兄弟節(jié)點是黑色,遠侄子是紅色的,近侄子是黑色的(我們期望的能夠一步達到的平衡條件,因為多出一個紅色節(jié)點,能夠通過染黑遠侄子,旋轉(zhuǎn)補償刪除的一側(cè))。以及兄弟節(jié)點是黑色,遠侄子是黑色的,近侄子是紅色的。

那么我們來考慮一下,當(dāng)兄弟節(jié)點是黑色,下面兩個侄子都是黑色的時候怎么辦?我們沒有默認的紅色節(jié)點啊,沒辦法給刪除的那一側(cè)補償啊。

下面是紅黑樹的一部分:


情況四(兩個侄子都是黑色).png

我們刪除a的話。此時怎么辦?都是黑色。沒辦法補償右側(cè)啊。我們只能學(xué)習(xí)插入的情況六。先染一個紅色的節(jié)點出來,把希望寄托與上層。

但是我們選擇怎么染顏色呢?還記得我們的情況二這種一步達到的平衡的狀況,既然沒有,我們就創(chuàng)造一個出來。

我們本來就要把指針移動到a(父親)出,從上層尋找機會。那么此時的b就是相對與上層的遠侄子了。那么我們把此時c的兄弟節(jié)點,b染紅即可。這樣我們就創(chuàng)造了一個遠侄子是紅色的情況。

情況四.png

這樣我們又解決了一個新的情況。

如果兄弟是黑色的,且兩個侄子(兄弟兩個孩子)也是黑色的,則把兄弟染成紅色,把指針指向父親。此時就可以變化為接近情況二的狀態(tài),指針指向父親,讓父親從上層找機會跳針。

情況五

我們一直在探討兄弟是黑色的,假如兄弟是紅色又怎么辦。

下面是紅黑樹的某一部分:


情況五.png

感覺此時的情況很好解決。因為此時兄弟本來就是紅色的,也就是說本來就又一個紅色節(jié)點提供給我們。如果能夠搬到把這個節(jié)點補償?shù)搅硪粋?cè)就完成。

但實際上我們思考一下就明白了,為什么我們上面要以遠侄子為紅色而不是兄弟而紅色呢?實際上很簡單,我們紅兄弟染黑,通過左旋和右旋,此時兄弟會成為這個子樹的根,會導(dǎo)致兩側(cè)都增加黑色節(jié)點,這樣還是不符合我們的邏輯。因此此時我們只能嘗試著把這種情況往情況二,三,四變化。

因此我們嘗試著把兄弟染黑,父親染紅


情況五.png

我們再對父親c進行右轉(zhuǎn):


情況五.png

這樣我們不斷的經(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);
測試結(jié)果.png

我們來試試分解步驟進行解析


紅黑樹刪除分解步驟例子.png

根據(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)自己也行的,沒有想象的這么難。

這次紅黑樹的盲敲讓我明白了,很多看起來困難的事情,只要自己一步一腳印的去做,或許能達到意想不到的效果呢。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內(nèi)容