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

背景

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

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

image.png

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

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

情況一.png

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

第二種情況:

情況二.png

遇到這種情況,怎么辦?為了保證性質(zhì)5.我們?cè)囋嚢驯竟?jié)點(diǎn)以外的節(jié)點(diǎn)的一些節(jié)點(diǎn)染黑看看,最后為了性質(zhì)3,再把葉子節(jié)點(diǎn)變黑,能不能達(dá)到平衡。

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


情況二第一次變化.png

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


情況二第二次變化.png

好像整個(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)

情況三:

情況三.png

這樣這種情況就分出一個(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è)又怎么分析的。

情況六

image.png

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


測(cè)試結(jié)果.png

我們分解一下步驟,看看這個(gè)過(guò)程是否正確。

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

根據(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ì)遇到什么情況吧。

情況一

image.png

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

接下來(lái)我們來(lái)考慮移除黑色節(jié)點(diǎn)時(shí)候該怎么處理。

情況二

情況二.png

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

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

情況二變化一.png

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

這種情況應(yīng)該是特殊情況。我們?cè)倏纯雌渌那闆r


情況二變化二.png

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

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

情況二變化三.png

也就是說(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è)紅黑樹的一部分


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

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

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

關(guān)鍵是我們?cè)撛趺丛诓挥绊憳涞钠胶獾那闆r下,轉(zhuǎn)化為情況二

情況三變化一.png

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

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

我們?cè)囋囘@樣的方式。我們?nèi)竞诮蹲樱炯t兄弟,進(jìn)行左旋。一樣能夠辦到上面的情況

情況三變化二.png

此時(shí)就是我們想要的情況,我們要?jiǎng)h除下方的c節(jié)點(diǎn),此時(shí)兄弟是黑色,遠(yuǎn)侄子是紅色,同時(shí)近侄子是黑色。這樣我們就進(jìn)入到了情況二。

我們?cè)侔研值苋境筛赣H的顏色,父親再染黑,遠(yuǎn)侄子再染黑,右旋整個(gè)樹


情況三變化到情況二.png

我們數(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ǔ)償啊。

下面是紅黑樹的一部分:


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

我們刪除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)侄子是紅色的情況。

情況四.png

這樣我們又解決了一個(gè)新的情況。

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

情況五

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

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


情況五.png

感覺此時(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í)我們只能嘗試著把這種情況往情況二,三,四變化。

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


情況五.png

我們?cè)賹?duì)父親c進(jìn)行右轉(zhuǎn):


情況五.png

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

我們來(lái)試試分解步驟進(jìn)行解析


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

根據(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á)到意想不到的效果呢。

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

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