如果HR再問紅黑樹,你就把《算法導論》拿出來批判一番

《算法導論》中第一次引入樹這種數據結構是以二叉堆的形式出現的,並且在附錄中在圖的基礎上給出了樹在數學上的定義。如果從鏈表的觀點出發,那麼相當於是放寬了有序的要求,即允許兩個不同位置的元素有相等的序。

對於序爲nn的節點來說,可以指向多個序爲n+1n+1的節點,相應的後者稱爲前者的孩子,前者稱爲後者的父節點。其最大序即爲樹的高度。(下圖中數字表示的是節點的代數,而非值)

在這裏插入圖片描述

在上圖中,0節點的左右兩個節點分別爲其左子節點和右子節點,反過來0節點也是這兩個子節點的父節點;左側的1節點的兩個2節點也分別爲其左子節點和右子節點,以此類推。

很容易發現,在一個樹中,只有0節點沒有父節點,這個節點被稱爲根節點。

二叉搜索樹

二叉搜索樹要求父節點大於等於其左子節點及其所有子節點,而小於等於其右子節點及其所有子節點,如下圖所示

在這裏插入圖片描述

初始化

如果想要在這個樹中查詢任意一個值,其最壞的情況也無非是查詢到最下面的點,進行的比較次數爲樹的高度。由於這是二叉樹,設樹的元素個數爲nn,則理想情況下樹的高度不大於log2n\log_2n

對於二叉搜索樹中的每個父節點最多子節有兩個子節點,樹中任意節點有三個指針,分別指父向節點、左子節點和右子節點。其中,根節點沒有父節點。C語言實現爲

//cTrees.c
typedef struct TREENODE
{
    struct TREENODE *father;
    struct TREENODE *left;
    struct TREENODE *right;
    int value;
}tNode;

現在,我們迫切地希望能夠實現一個二叉搜索樹,但正如此前所接觸的線性數據結構一樣,生成樹的過程也必然始於添加節點。

對於一個已有的二叉搜索樹而言,當我們插入一個新節點的時候,應該比較新節點與當前節點的值,如果大於當前節點,則比較新節點與當前節點右子節點的值;如果小於當前節點,則比較新節點與當前節點左子節點的值。如果下一個將要比較的節點不存在,那麼正好可以把新節點插進來。

void insertNode(tNode* root, int val){
    tNode* new = (tNode*)malloc(sizeof(tNode));
    new->value=val;
    new->left=NULL;
    new->right=NULL;
    while (TRUE){
        if (root->value<val) 
            if(root->right!=NULL)
                root=root->right;
            else{
                //若右子節點不存在,則新節點成爲其右子節點
                new->father = root;
                root->right = new;
                return;         //賦值之後函數結束
            }
        else                    //左邊的操作與右邊相同
            if (root->left!=NULL)
                root=root->left;
            else{
                new->father=root;
                root->left=new;
                return;
            }
    }
}

當能夠生成二叉搜索樹之後,我們更迫切地想知道這個二叉搜索樹生成的對不對,所以需要打印這個二叉搜索樹

//打印二叉搜索樹,輸入爲節點和節點序
void printBST(tNode* root,int start){
    printf("the %dth node is %d\n",start,root->value);
    //如果當前節有子節點,則繼續打印這個子節點和節點序
    if (root->left!=NULL)
        printBST(root->left,start+1);
    if (root->right!=NULL)
        printBST(root->right,start+1);
}

最終在主函數中進行驗證

int main(){
    tNode* root;
    root->left=NULL;
    root->right=NULL;
    root->value=10;     //以上初始化根節點

    int init[10]={1,11,5,12,2,4,19,11,8,7};
    for (int i = 0; i < 10; i++)
        insertNode(root,init[i]);
    
    printBST(root,0);
    return 0;
}

其結果爲

PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe       
the 0th node is 10
the 1th node is 1       //第一代節點,10的左子節點
the 2th node is 5       //第二代節點,1的右子節點
the 3th node is 2       //第三代節點,5的左子節點
the 4th node is 4       //第四代節點,2的右子節點
the 3th node is 8       //第三代節點,5的右子節點
the 4th node is 7       //第四代節點,8的左子節點
the 1th node is 11      //第一代節點,10的又子節點
the 2th node is 11      //第二代節點,11的左子節點
the 2th node is 12      //第二代節點,11的右子節點
the 3th node is 19      //第三代節點,12的右子節點
PS E:\Code\AlgC>

我們生成的樹爲

在這裏插入圖片描述

可見的確符合二叉搜索樹的規則。那麼接下來我們需要爲這棵二叉樹添加新的功能,即搜索節點與刪除節點。其中,搜索功能與插入功能如出一轍,區別只是在於,我們不需要重複插入,我們只需將這個值的指針返回即可。同時,循環判定也變爲while(root->value!=val)

//通過節點的值搜索節點地址,root爲根節點
tNode* searchBST(tNode* root, int val){
    while (root->value!=val)
    {
        if (root->value<val && root->right!=NULL)
            root=root->right;
        else if (root->value>val && root->left!=NULL)
            root=root->left;
        else
            return FALSE;
    }
    return root;
}

刪除節點

相比之下,刪除節點顯得更加複雜一些,因爲此時將涉及到其父節點、左子節點、右子節點以及兄弟節點之間的大小關係。

如果被刪除的節點沒有子節點,那當然皆大歡喜,只需將其父節點指向被刪除節點的指針變成NULL即可;如果只有一個子節點,也並不麻煩,只需指向被刪除節點的指針指向這個子節點。

然而,如果有兩個子節點,那麼由於父節點必須大於左子節點而小於右子節點,所以取代被刪除節點的一可以是左子節點,也可以是右子節點。區別在於,若是右子節點取代該節點,則左子節點爲新父節點的左子節點;若是左子節點取代父節點,則右子節點仍爲新父節點的右子節點。

所以,二選其一即可,交換當前節點與其右子節點,然後刪除交換後的右子節點即可,如果交換後的右子節點仍然有兩個子節點,則繼續交換,直到能夠刪除爲止。這裏其實有一個巧妙的默認,即默認爲待刪除節點無論處於什麼位置都是合法的,畢竟這個節點最終將被刪除掉。

//刪除節點的值,root爲根節點,delNode爲待刪除節點
void deleteNode(tNode* delNode){
    if(delNode->left==NULL&&delNode->right==NULL){
        if(delNode->value>pNode->value)
            pNode->right=NULL;
        else
            pNode->left=NULL;
    }
    else if(delNode->left!=NULL&&delNode->right!=NULL){
        int val = delNode->value;
        //交換當前節點與右節點的值
        delNode->value = delNode->right->value;
        delNode->right->value=val;
        deleteNode(delNode->right);//刪除右節點
    }
    else{
        tNode* pNode = (delNode->left==NULL) \
            ? delNode->right : delNode->left;
        delNode->value = pNode->value;
        delNode->right = pNode->right;
        delNode->left = pNode->left;
    }
}

驗證一下

int main(){
    tNode* root;
    root->left=NULL;
    root->right=NULL;
    root->value=10;
    int init[10]={1,11,5,12,2,4,19,11,8,7};
    for (int i = 0; i < 10; i++)
        insertNode(root,init[i]);
    
    tNode* sNode=searchBST(root,5);
    deleteNode(sNode);
    printBST(root,0);
   return 0;
}

結果爲

PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe       
the 0th node is 10
the 1th node is 1
the 2th node is 8
the 3th node is 2
the 4th node is 4
the 3th node is 7
the 1th node is 11
the 2th node is 11
the 2th node is 12
the 3th node is 19

示意圖爲

在這裏插入圖片描述

旋轉節點

二叉搜索樹有一個問題,如果我們在對其進行初始化的時候,輸入的是1,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,9,那麼這個所謂的二叉樹可能並不會產生叉,而是徑直變成一個只有右子節點的鏈表。

由於二叉樹的時間複雜度與其樹高是成正比的,所以不分叉的二叉樹也會失去時間複雜度上的優勢。所以,如果能夠控制二叉樹的高度,使之橫向分佈儘量均勻,就能夠有效提高其性能。

最直觀的想法是,假設yyxx的右子節點,而xx的左子節點的家族人丁稀薄,yy的右子節點子系繁多,那麼如果把yy的子系過繼給xx,或者乾脆取代xx,父子關係逆轉,必定能使得整個樹變得更加均勻。

在這裏插入圖片描述

現考慮x,xL,y,yL,yRx,x_L,y,y_L,y_R這五個節點,其中xL,yx_L,yxx的左右節點,yL,yRy_L,y_Ryy的左右節點。那麼這些節點之間必然存在關係xLxyLyyRx_L\leqslant x\leqslant y_L\leqslant y\leqslant y_R。如果希望yy變爲xx的父節點,那麼xx必然是yy的左子節點。此時yy將多出一個節點,必須過繼給xx,又因爲xyx\leqslant y,所以只能過繼左子節點yLyL

這裏可以考慮插入一箇中間步以便於理解

在這裏插入圖片描述

可見yy多了一個節點yLy_L正好可以過繼給xx,成爲其左子節點。則過繼之後的節點關係變成

在這裏插入圖片描述

可見這個過程並沒有改變二叉搜索樹的性質,但是在yRy_R長於yLy_L的情況下,能夠有效降低樹的高度。

因爲x,yx,y的轉置過程就像旋轉一樣,所以這個操作叫做旋轉,又因爲父節點變成了子節點的左子節點,所以叫左旋,其逆過程就是右旋。但本文中並不提倡左旋右旋這種故作高深的提法,而是提倡旋轉x,yx,y兩個節點這種說法。

總之可能是翻譯的腦補能力很強,所以起了這麼個奇葩的名字。其實這個操作的本質就是這五個點的重新排布而已,硬把這種重新排布命名成旋轉我也是醉了,rotationrotation就算翻譯成轉置\textbf{轉置}也要比旋轉更加貼切,也能減少理解上的困難。

旋轉操作落實到算法上,其實並不需要考慮xL,yRxL,yR,但需要考慮考慮xx父節點指針的變化。即整個操作過程無非是x,y,yLx,y,y_L以及xx父節點的指針變化而已,傳統的旋轉實現爲

#define RIGHT 1

#define LEFT 0

//樹節點的經典旋轉操作,flag爲LEFT時左旋,RIGHT時右旋
void rotNode(tNode *xNode, int flag){
    tNode *yNode;
    if (flag==LEFT){
        yNode = xNode->right;
        xNode->father->right = yNode;//y成爲x父節點的右子節點
    }else{
        yNode = xNode->left;
        xNode->father->left = yNode;
    }

    yNode->father = xNode->father; //x的父節點成爲y的父節點
    xNode->father = yNode;         //y成爲x的父節點

    if (flag == LEFT){
        yNode->left->father = xNode; //y左子節點過繼給x
        xNode->right = yNode->left;
        yNode->left = xNode;
    }else{
        yNode->right->father = xNode;
        xNode->left = yNode->right;
        yNode->right = xNode;
    }
}

將主函數中刪除操作代碼換成rotNode(sNode,LEFT);,其結果爲

PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe       
the 0th node is 10
the 1th node is 5
the 2th node is 1
the 3th node is 2
the 4th node is 4
the 2th node is 8
the 3th node is 7
the 1th node is 11
the 2th node is 11
the 2th node is 12
the 3th node is 19

可見5的確成爲了1的父節點,而2過繼給了1,樹高並沒有變化,是因爲原5的左右子族的長度相等。從代碼的角度來說,替換意義上的旋轉操作可以更加簡潔,而且更有利於理解。

//替換意義上的旋轉操作,sNode爲子節點,pNode爲父節點
void turnNode(tNode *sNode, tNode *pNode){
    if(sNode==pNode->right){
        sNode->left->father = pNode;
        pNode->right = sNode->left;
        sNode->left = pNode;
    }else{
        sNode->right->father = pNode;
        pNode->left = sNode->right;
        sNode->right = pNode;
    }
    sNode->father = pNode->father;
    pNode->father = sNode;
}

紅黑樹

調整節點

有了旋轉操作,那麼問題便成了何時旋轉。最直觀的方案是,讓每個節點都包含輩分信息,然後想辦法讓每一個家族的輩分相差不要太過懸殊。這種方案的問題是,如果改變一個父節點的輩分,那麼這個父節點的所有子孫,將都會受到影響。

所以,我們需要找到某中共衡量樹高的某個參數,並且這個參數易於保持。由於樹的節點數目並不固定,所以不同子孫所構成鏈表的長度也必然不等,要求每個家族的最小輩分完全相等是不現實的,唯一能夠做到的是,讓每個家族在抽離一些特殊的子女之後,達到輩分相等。

紅黑樹便是本着這樣一種思維,這裏要求任意一個父節點到其最後一代節點的所有簡單路徑中,包含相同數目的黑色節點。考慮到父節點到其後低啊的所有簡單路徑不可能包含相同的節點,所以要在黑色節點之間插入紅色節點,以保證黑色節點數目相等。

但是,紅色節點不能亂插,並且必須要少於黑色節點,所以要求紅色父節點的兩個子節點都爲黑色

首先,定義一下紅黑樹的節點。

#define RED 0
#define BLACK 1
typedef struct rbNODE
{
    struct rbNODE* father;
    struct rbNODE* left;
    struct rbNODE* right;
    int value;
    int color;
}rbNode;

現在,就可以嘗試生成一棵紅黑樹。像二叉搜索樹一樣,首先要有一個根節點,由於根節點對於所有子孫節點都是唯一的,所以選則黑色即可。

便於指導後面的操作,將紅黑樹的兩點要求列在此處

  1. 任意父節點到其最後一代孫節點的所有簡單路徑中,黑色節點相同數目。
  2. 紅節點的左右子節點均爲黑色。

第一條性質也可以寫爲等價形式:任何一個末代孫節點到根節點的簡單路徑中,黑色節點數目相同。或者說,任何兩個末代孫節點抵達任意一個相同的祖節點的簡單路徑中,黑色節點數目相同。

  • 叔節點與父節點都爲紅色

然後,如果向已有的紅黑樹中插入新的節點NN,由於第一條規則,我們優先考慮紅色。如果這個NN的父節點PP也是紅色,那就尷尬了,違反了第二條規則,所以需要把PP變成黑色。但變成黑色之後,這條路徑就比其他路徑多了一條黑色節點。

這時如果PP的兄弟節點、NN的叔叔QQ是紅色節點就好辦了,可以將QQ也變成黑色,然後將P,QP,Q的父節點GG變成紅色。這樣,GG的所有子系就得到了統一,從而整棵樹都得到了統一。唯一可能麻煩的是,GG和其父節點可能會違反第二條規則,但這已經是GG的父輩和祖輩之間的事情了,重複調用即可。

  • 叔節點爲黑色

然而,如果QQ是黑色的,那麼問題會有些麻煩,這裏可以考慮一下旋轉操作,如下圖所示

在這裏插入圖片描述

假設xL,yL,yRx_L,y_L,y_R的子系均符合紅黑樹的要求,比較旋轉前後的各條子系

旋轉前 旋轉後
x,y,yLx,y,y_L y,x,yLy,x,y_L
x,xLx,x_L y,x,xLy,x,x_L
x,y,yRx,y,y_R y,yRy,y_R

可見,如果x,yx,y均爲紅色,則旋轉前後黑點的數目並不會發生變化;如果xx爲黑色,則y,yRy,y_R這條子系減少一個黑節點;如果yy爲黑色,則y,x,xLy,x,x_L這條子系增加一個黑節點。

總結一下,即兩個紅色節點的旋轉操作不會改變子系的黑色節點數目;紅父與右黑子的旋轉,會使紅父的左子節點的子系增加一個黑色節點;黑父與右紅子的旋轉,會使紅子的右節點減少一個黑色節點。這意味着當父子節點均爲紅色時,我們就可以大膽地使用旋轉操作而不必擔心出亂子

當父節點PP和子節點NN都爲紅色,且NN的叔節點QQ爲黑色時,我們可以嘗試旋轉一下節點P,NP,N,但P,NP,N旋轉之後並不會改變二者的顏色,二者仍舊不滿足第二條規則。但由於PP是紅色,那麼PP的父親GG一定爲黑色,而PP的兄弟節點QQ也爲黑色,所以只需PP變成黑色,讓GG變成紅色就能夠滿足第二條規則了。

但這裏又出現了新的問題,滿足第二條規則之後,GGQQ子系必然因爲GG的變色而少了一個黑色節點。考慮到P,GP,G二者的顏色,現在將這兩個節點再旋轉一次,正好能夠使得QQ子系增加一個節點,至此紅黑樹又重新滿足了要求。

總結一下,如果

  1. 叔節點QQ存在且爲紅色,則將父節點PP和叔節點QQ同時設爲黑色,將祖父節點GG設爲紅色,然後將指針指向祖父節點。

若叔節點QQ不存在或爲黑色,則

  1. 若插入節點與父節點在同側(例如,插入節點爲左節點,父節點也爲左節點),則將父節點PP設爲黑色,將祖父節點GG設爲紅色,旋轉P,GP,G
  2. 若插入節點與父節點在異側,則旋轉PP和插入節點,然後將指針移向PP,此時PP與其父節點成爲同側節點。
//調整紅黑樹的節點
//調整紅黑樹的節點
void adjustRBT(rbNode *node){
    rbNode *pNode = node->father; //父節點
    rbNode *qNode;                //叔節點
    while (pNode->color==RED){
        int flag = pNode == pNode->father->left 
                 ? LEFT : RIGHT;
        qNode = flag==LEFT ? pNode->father->right 
                           : pNode->father->left; //叔節點
        //如果叔節點存在且爲紅色
        if (qNode!=NULL || qNode->color==RED){
            pNode->color = BLACK;
            qNode->color = BLACK;
            pNode->father->color = RED;
            node = pNode->father;
            pNode = node->father;
        }
        else{
            if(flag != (node==pNode->left ? LEFT : RIGHT)){
                turnRbNode(node,pNode);//此時插入節點與父節點在異側
                node = pNode;
                pNode = node->father;
            }//執行完此操作後,變爲同側
            pNode->color=BLACK;
            pNode->father=RED;
            turnRbNode(pNode,pNode->father);
        }
    }
}

初始化

紅黑樹的根節點顏色並不會影響紅黑樹的第一條性質,但如果紅黑樹的根是紅色的,那麼其左子節點和右子節點必須同時爲黑色。所以,當根節點爲黑色時顯然對其後代顏色的影響更小,所以選取根色爲黑。

此前所定義的二叉樹的插入操作對紅黑樹完全有效,但是需要額外添加節點顏色。爲此,可以從二叉樹的插入函數中提取出新節點的指針,併爲這個指針賦予顏色,然後對這個指針的顏色進行調整。

當然,在真正啓動初始化程序之前,最好還是檢查一下此前的算法的漏洞。紅黑樹的核心算法adjustRBT中所引用的旋轉操作其實隱藏着一個很大的bug,即其默認在樹中間進行操作,所涉及到的所有的節點元素都不爲NULL,所以一旦涉及到根節點或者末代節點,就必然會引發災難。所以,必須在變化之前進行節點判斷

//打印紅黑樹,使之能夠顯示紅黑特性
void printRBT(rbNode *root, int start){
    printf("the %dth node : %d with %d\n", start, root->value, root->color);
    if (root->left != NULL)
        printRBT(root->left, start + 1);
    if (root->right != NULL)
        printRBT(root->right, start + 1);
}
//旋轉紅黑樹的節點,sNode是被旋轉的子節點
//root爲根節點,輸出爲旋轉後的根節點
rbNode* turnRbNode(rbNode *root, rbNode *sNode){
    rbNode* pNode = sNode->father;      //被旋轉的父節點
    if(sNode==pNode->right){            //s爲右子節點
        if(sNode->left!=NULL)
            sNode->left->father = pNode;//s的左子節點過繼給pNode
        pNode->right = sNode->left;     //p接收s的左子節點
        sNode->left = pNode;            //p成爲s的左子節點
    }else{
        if (sNode->right!=NULL)
            sNode->right->father = pNode;
        pNode->left = sNode->right;
        sNode->right = pNode;
    }
    sNode->father = pNode->father;  //sNode過繼給pNode的父節點
    pNode->father = sNode;          //pNode和sNode父子逆轉
    
    if (sNode->father==NULL)        //若pNode爲根節點
        return sNode;
    
    if (pNode==pNode->father->right)
        sNode->father->right = sNode;
    else
        sNode->father->left = sNode;
    return root;
}
//紅黑樹的插入算法
rbNode* insertRbNode(rbNode *root, int val){
    rbNode *new = (rbNode *)malloc(sizeof(rbNode));
    new->value = val;
    new->left = NULL,new->right = NULL;
    new->color = RED;

    rbNode *tmpRoot = root;         //保護root
    rbNode *temp;
    while (temp = root->value < val 
            ? root->right : root->left,temp!=NULL)
        root = temp;
    new->father = root;
    if (root->value < val)
        root->right = new;
    else
        root->left = new;

    return adjustRBT(tmpRoot,new);
}

主函數爲

//紅黑樹插入算法
int main(){
    rbNode Root = {NULL,NULL,NULL,11,1};
    rbNode* root = &Root;
    int init[7]={2,14,1,7,15,5,8};
    for (int i = 0; i < 7; i++){
        root = insertRbNode(root,init[i]);
    }
    root = insertRbNode(root,4);
    printRBT(root,0);
    return 0;
}

輸出爲

PS E:\Code\AlgC> .\a.exe       
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 1th node : 11 with 1
the 2th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 15 with 0

這裏選取的參數與《算法導論》中一致,可以通過調試看出節點以及節點顏色的變化過程,與《算法導論》中所列出的基本一致,所以就不畫圖了。

在這裏插入圖片描述

刪除節點

回顧一下二叉搜索樹的節點刪除操作,可以發現,如果被刪除節點有兩個節點,則這個節點將與其子節點的值進行交換。這種交換終止於交換後的子節點不多於一個子節點。

當然,這個過程可以改得更加激進一些,即只要被刪除節點仍有子節點,那麼就將該節點與子節點的值進行交換,然後將指針指向子節點,直到指針指向末代節點,然後刪除。

如果將這種操作挪用到紅黑樹上,那麼在值交換的過程中,並不必交換節點顏色,於是只有最終刪除末代節點的時候,才需要考慮節點顏色。

然而,末代節點被刪除將導致末代節點這條世系徹底消失,所以,無論末代節點的顏色如何,都不會改變其他世系的黑高,所以我們驚奇地發現,別穿得難乎其難的紅黑樹刪除節點操作,竟然簡單的讓人難以置信。

//紅黑樹查詢,root爲根節點,val爲待查詢值
//返回值爲節點的指針
rbNode* searchRBT(rbNode *root, int val){
    if(root->value==val)
        return root;
    if(root->value<val && root->right!=NULL)
        return searchRBT(root->right,val);
    else if(root->value>val && root->left!=NULL)
        return searchRBT(root->left,val);
    else
        return FALSE;
}
//紅黑樹刪除節點,輸入爲待刪除節點指針
void deleteRbNode(rbNode* dNode){
    rbNode *pNode = dNode->father;
    if (dNode->left == NULL && dNode->right == NULL){
        if (dNode==pNode->right)
            pNode->right = NULL;
        else
            pNode->left = NULL;
    }
    else{
        //如果左子節點存在,則pNode爲dNode的左子節點,否則爲右子節點
        pNode = (dNode->left==NULL) ? dNode->right : dNode->left;
        int val = dNode->value;
        dNode->value = pNode->value;
        pNode->value = val;
        deleteRbNode(pNode);
    }
}
//主函數
int main()
{
    rbNode Root = {NULL,NULL,NULL,11,1};
    rbNode* root = &Root;
    int init[10]={2,14,1,7,15,5,8,4,13,6};
    for (int i = 0; i < 10; i++){
        root = insertRbNode(root,init[i]);
    }
    
    rbNode* delNode = searchRBT(root,11);
    printRBT(root,0);
    deleteRbNode(delNode);
    printf("after delete node 11\n");
    printRBT(root,0);
   return 0;
}

結果爲

PS E:\Code\AlgC> gcc .\cTrees.c
PS E:\Code\AlgC> .\a.exe       
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 3th node : 6 with 0
the 1th node : 11 with 1
the 2th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 13 with 0
the 3th node : 15 with 0
after delete node 11
the 0th node : 7 with 1
the 1th node : 2 with 1
the 2th node : 1 with 1
the 2th node : 5 with 1
the 3th node : 4 with 0
the 3th node : 6 with 0
the 1th node : 8 with 1
the 2th node : 14 with 1
the 3th node : 13 with 0
the 3th node : 15 with 0

順序統計樹

順序統計樹是基於紅黑樹的一種數據擴張,除了紅黑屬性之外,其節點還包含子繫個數的信息sizesizesizesize即以當前節點爲根的子樹的所有節點個數。

在經歷了實現紅黑樹的折磨之後,這種擴張顯得輕而易舉。首先在節點結構體中添加一個成員size。然後修改插入操作,當插入新節點時,新節點的size值爲1,途中經歷的所有指針指向的節點,其size值都加1。

//rbNode* insertRbNode(rbNode *root, int val);
//...
    while (temp = root->value < val 
            ? root->right : root->left,temp!=NULL){
        root->size += 1;
        root = temp;
    }
//...

刪除操作時,記錄最終被刪除的節點指針,其所有父輩的size均減一。

    if (dNode->left == NULL && dNode->right == NULL){
        if (dNode==pNode->right)
            pNode->right = NULL;
        else
            pNode->left = NULL;
        while(pNode.size-=1,pNode->father!=NULL){
            pNode = pNode->father;
        }
    }

size值一方面給出了當前節點子系的體量,另一方面也是對當前節點在所有節點中的大小排名的一個標記。當指針從根節點依次下沉時,順帶也繼承了當前節點的區間信息,其實現爲

rbNode* searchRBTN(rbNode *root, int n){
    int low = 0;    //左開右閉
    int high = root->size;
    if(n>high)
        return NULL;

    while (1){
        //左子節點存在且size<n-low
        if (root->left!=NULL && root->left->size<n-low){
            root = root->left;
            high = low + root->size;
        }else{
            root = root->right;
            low = high - root->size;
        }
        if (root->right!=NULL && root->right->size==high-root->size)
            return root;
        if(root->left!=NULL && root->left->size == low+root->size-1)
            return root;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章