動態順序統計

在之前的隨機選擇算法中,我們可以很快的在集合中尋找到第i小的元素,然而,這樣的集合並不支持動態的擴充。這一節裏,將介紹通過紅黑樹(具體可參考紅黑樹1紅黑樹2兩篇文章)的擴充,使得任意的順序統計量都可以在短時間內查找到,而這樣的數據結構同時也支持數據的更新。

這樣的數據結構稱爲順序統計樹,如下圖。樹的節點大致上與紅黑樹的類似,但增加了一個記錄子樹大小的域size[x],定義哨兵Nil的子樹大小爲0,即size[Nil[T]]=0。有等式size[x]=size[left[x]]+size[right[x]]+1


順序統計樹主要有兩種算法:

1.檢索具有給定排序的元素

其實就是x的順序就保存在他的左孩子的節點上,所以每次和左孩子記錄的值進行比較,匹配成功,則直接返回;比左孩子小了,那麼在做孩子處遞歸調用算法;否則在右孩子處遞歸調用算法。

2.確定一個元素的秩

和上一個差不多,只是傳入的節點,返回的節點所處的位置。

所有節點在插入的時候時需要注意,因爲插入的過程就是和某一個節點比較大小,如果小於這個節點,那麼就往左孩子處遞歸調用算法,否則的話向右孩子處遞歸。每次遞歸的時候,都需要給當前節點的size位加上1。一直遞歸到底層,這個時候,由於紅黑樹的插入性質,需要做左旋或者右旋的一些操作(顏色的改變不會影響到子樹的大小),旋轉之後需要重新確定好節點的大小。

下面給出代碼(基於之前的紅黑樹的,所以嘛有點長==):

PS:其實改動的代碼不到100行,其他的就是紅黑樹的源代碼

#include <stdio.h>
#include <stdlib.h>

typedef enum Color{Red,Black}Color;

/*這裏定義樹的結構,每個節點爲Node結構體,再加上一個頭指針Tree*
在紅黑樹中,多了一個顏色的域Color,這裏用枚舉表示
順序統計數中,多了一個表示子樹大小的Size*/
typedef struct Node
{
    int data;
    Color color;
    struct Node* left;
    struct Node* right;
    struct Node* parent;
    int Size;
}Node;

Node Nil={0,Black,NULL,NULL,NULL};

typedef struct Tree
{
    Node* Root;
}Tree;


/*一下的操作主要針對普通的二叉搜索樹進行,但一些搜索、前去後繼等操作也是可以直接用的*/


/*對樹進行中序遍歷*/
void Mid_Traverse(Node* Root)
{
    if(Root!=&Nil)
    {
        Mid_Traverse(Root->left);
        printf("%d ",Root->data);
        Mid_Traverse(Root->right);
    }
}


/*普通二叉樹的插入操作,對紅黑樹不能用這個函數!*/
/*以下函數是對樹進行插入操作
定義兩個Node變量x和y,一開始x指向根節點,y爲空
然後將x的值一次往下遞減向左邊下降還是右邊依據和z的比較,而y的值一直都是x的父節點,以防當x爲空時,就找不到這棵樹了
然後讓z的父節點指向y,相當於把z放到x的地方
當然,需要判斷這棵樹是否一開始就是空的,如果y是空的話,那麼直接把更節點給z
否則的話更具z的值與y比較大小,判斷是把z放到左邊還是右邊*/
void Tree_Insert(Tree* T,Node* z)
{
    Node* y=NULL;
    Node* x=T->Root;
    while(x!=NULL)
    {
        y=x;
        if(z->data<x->data)
            x=x->left;
        else
            x=x->right;
    }
    z->parent=y;
    if(y==NULL)
    {
        T->Root=z;
    }
    else
    {
        if(z->data<y->data)
            y->left=z;
        else
            y->right=z;
    }
}

/*查找函數,從根節點進行遞歸查找,當查找的當前節點爲空或者節點就是要找的那個的話,停止查找
否則向下進行查找,向左邊還是向右邊取決於節點的值與k的比較*/
Node* Tree_Search(Node* Root,int k)
{
    if(Root==NULL||k==Root->data)
        return Root;
    if(k<Root->data)
        return Tree_Search(Root->left,k);
    else
        return Tree_Search(Root->right,k);
}

/*下面兩個函數返回樹的最小值和最大值,就是一直往左走或者一直往右走就行了*/
Node* Tree_Minimum(Node* Root)
{
    while(Root->left!=&Nil)
        Root=Root->left;
    return Root;
}

Node* Tree_Maximum(Node* Root)
{
    while(Root->right!=&Nil)
        Root=Root->right;
    return Root;
}

/*某一個節點的後繼的查找
如果這個節點的右孩子不爲空的話,那麼只要以右孩子爲根節點,返回右子樹的最小值就行了
否則的話,就要向上回溯,節點y首先指向x的父節點
只要y不爲空(此時到了根節點了,直接拿來就行了),並且x是y的右孩子(說明了x的值還是大於y的。。)的話,就一直向上回溯
兩種情況停止循環:一個是到達了根節點了,中序遍歷的話此時下一個節點必然是根節點
另一種情況是當x是y的左孩子,那麼y的是就是大於x的了,那麼x的下一個元素必然是y了*/
Node* Tree_Successor(Node* x)
{
    if(x->right!=&Nil)
        return Tree_Minimum(x->right);
    Node* y=x->parent;
    while(y!=&Nil&&x==y->right)
    {
        x=y;
        y=y->parent;
    }
    return y;
}

/*前驅的查找與上面的分析類似*/
Node* Tree_Predecessor(Node* x)
{
    if(x->left!=&Nil)
        return Tree_Maximum(x->left);
    Node* y=x->parent;
    while(y!=&Nil&&x==y->left)
    {
        x=y;
        y=y->parent;
    }
    return y;
}

/*普通的二叉搜索樹的刪除操作,紅黑樹不能用這個! */
/*節點的刪除操作,前面幾行算法首先確定需要刪除的元素y,z有兩個孩子的話那麼刪除z的後繼,否則直接刪除z
然後將x置爲y的非空子女,若果y無子女的話,那麼x就設置爲空
如果x非空的話,通過修改指針將y刪除
否則的話還要考慮邊界情況,若果要刪除的y是根節點的話,那麼直接把根節點給x(注意,x要麼爲空,要麼就是y的唯一一個子樹)
如果y是左孩子的話,那麼把x放在y的父節點的左孩子位置上,反之放在右孩子上
最後判定,如果y是z的後繼的話,就是說刪除掉的節點不是z的話,那麼要把z的值賦值給y*/
Node* Tree_Delete(Tree* T,Node* z)
{
    Node* y;Node* x;
    if(z->left==NULL||z->right==NULL)
        y=z;
    else
        y=Tree_Successor(z);
    if(y->left!=NULL)
        x=y->left;
    else
        x=y->right;
    if(x!=NULL)
        x->parent=y->parent;
    if(y->parent==NULL)
        T->Root=x;
    else
    {
        if(y==y->parent->left)
            y->parent->left=x;
        else
            y->parent->right=x;
    }
    if(y!=z)
        z->data=y->data;
    return y;
}

/*以下是對節點x進行左旋左旋操作
先完成Y的左孩子到X的連接,首先用節點Y指向X的右孩子,把Y的左孩子放到X的右孩子處
判斷,如果Y的左孩子是不空的話,那麼直接把X作爲Y的左孩子的父節點
然後完成Y節點和X的父節點的連接。把Y的父節點直接連向X的父節點,當然,如果X的父節點是空的話,那麼根節點就是Y
判斷兩種情況,如果X是左孩子的話,那麼那麼Y就是左孩子,否則Y是右孩子
最後完成X於Y的連接,把X的父節點爲Y,Y的左孩子爲X
注意,順序統計數中需要加上最後兩行代碼,右旋也是如此*/
void Left_Rotate(Tree* T,Node* X)
{
    Node* Y=X->right;
    X->right=Y->left;
    if(Y->left!=&Nil)
        Y->left->parent=X;
    Y->parent=X->parent;
    if(X->parent==&Nil)
        T->Root=Y;
    else if(X->parent->left==X)
        X->parent->left=Y;
    else
        X->parent->right=Y;
    Y->left=X;
    X->parent=Y;
    Y->Size=X->Size;
    X->Size=X->left->Size+X->right->Size+1;
}

/*右旋操作,和左旋操作完全一樣,代碼是對稱的*/
void Right_Rotate(Tree* T,Node* Y)
{
    Node* X=Y->left;
    Y->left=X->right;
    if(X->right!=&Nil)
        X->right->parent=Y;
    X->parent=Y->parent;
    if(Y->parent==&Nil)
        T->Root=X;
    else if(Y->parent->left==Y)
        Y->parent->left=X;
    else
        Y->parent->right=X;
    X->right=Y;
    Y->parent=X;
    X->Size=Y->Size;
    Y->Size=X->left->Size+X->right->Size+1;
}

/*以下是對紅黑樹插入之後的修正操作
下面的循環條件就是按照之前的那三種情形來實現的
首先,判讀z的父節點顏色是否爲紅色的,如果是黑色的話,就能不需要任何修正,但如果是紅色的話,就要進行下一步
判斷z的父節點是爺爺節點的左孩子還是右孩子,這樣就區分爲上面曾講到的情形A和情形B
首先判斷的是情形A,B的話與之類似就不講了。在情形A中,父節點處於左孩子位置上,接下來的一步,就要判斷z的大叔節點了
令Y等於z的大叔節點,就是z的爺爺節點的右孩子(情形A),如果大叔節點是紅色的話,那麼恭喜可以直接重新上色,爲情形1
但如果大叔節點不是紅色的呢?那麼就要判斷情形2還是情形3
如果z是右孩子的話,那麼就是情形2,此時對z的父節點進行左旋操作,並直接把z指向他的父節點。
然後重新上色,把z的父節點上成黑色,爺爺節點上成紅色,然後對爺爺節點進行右旋操作即可
如果z是左孩子的話,直接就是第三種情形,直接右旋即可*/
void RB_Insert_Fixup(Tree* T,Node* z)
{
    Node* Y;
    while(z->parent->color==Red)
    {
        if(z->parent==z->parent->parent->left)
        {
            Y=z->parent->parent->right;
            if(Y->color==Red)
            {
                z->parent->color=Black;
                Y->color=Black;
                z->parent->parent->color=Red;
                z=z->parent->parent;
            }
            else
            {
                if(z==z->parent->right)
                {
                    z=z->parent;
                    Left_Rotate(T,z);
                }
                z->parent->color=Black;
                z->parent->parent->color=Red;
                Right_Rotate(T,z->parent->parent);
            }
        }
        else if(z->parent==z->parent->parent->right)
        {
            Y=z->parent->parent->left;
            if(Y->color==Red)
            {
                z->parent->color=Black;
                Y->color=Black;
                z->parent->parent->color=Red;
                z=z->parent->parent;
            }
            else
            {
                if(z==z->parent->left)
                {
                    z=z->parent;
                    Right_Rotate(T,z);
                }
                z->parent->color=Black;
                z->parent->parent->color=Red;
                Left_Rotate(T,z->parent->parent);
            }
        }
    }
    T->Root->color=Black;
}



/*紅黑樹的插入操作,除去最後兩行外,其餘的和普通的二叉樹插入是一樣的
最後做了兩個工作,1.將插入的節點z的顏色設置成紅色2.調用RB_Insert_Fixup函數進行修正*/
void RB_Insert(Tree* T,Node* z)
{
    Node* Y=&Nil;
    Node* X=T->Root;
    while(X!=&Nil)
    {
        Y=X;
        if(z->data<X->data)
        {
            X->Size++;
            X=X->left;
        }
        else
        {
            X->Size++;
            X=X->right;
        }
    }
    z->parent=Y;
    if(Y==&Nil)
    {
        z->color=Black;
        T->Root=z;
        return;
    }
    else if(z->data<Y->data)
        Y->left=z;
    else
        Y->right=z;
    z->left=&Nil;
    z->right=&Nil;
    z->color=Red;
    RB_Insert_Fixup(T,z);
}


/*傳遞的節點x有兩種情況,在y被刪除之前,如果y有個不是哨兵Nil的節點,那麼x就是y的唯一的孩子
如果y沒有孩子,那麼x就是哨兵Nil。但無論x是什麼值,x的父節點都是先前y的父節點*/
void RB_Delete_Fixup(Tree* T,Node* x)
{
    Node* w;
    while(x!=T->Root&&x->color==Black)
    {
        if(x==x->parent->left)
        {
            w=x->parent->right;/*進入情況1*/
            if(w->color==Red)
            {
                w->color=Black;
                x->parent->color=Red;/*交換顏色後進行左旋操作,新的w爲x的新的兄弟節點*/
                Left_Rotate(T,x->parent);
                w=x->parent->right;
            }
            if(w->left->color==Black&&w->right->color==Black)/*情況2*/
            {
                w->color=Red;
                x=x->parent;
            }
            else
            {
                if(w->right->color==Black)/*右孩子是黑色的,進入情況3*/
                {
                    w->left->color=Black;
                    w->color=Red;
                    Right_Rotate(T,w);
                    w=x->parent->right;
                }
                else
                {
                    w->color=x->parent->color;/*進入情況4*/
                    x->parent->color=Black;
                    w->right->color=Black;
                    Left_Rotate(T,x->parent);
                    x=T->Root;
                }
            }
        }
        else/*剩下的部分是完全對稱的*/
        {
            w=x->parent->left;
            if(w->color==Red)
            {
                w->color=Black;
                x->parent->color=Red;
                Right_Rotate(T,x->parent);
                w=x->parent->left;
            }
            if(w->right->color==Black&&w->left->color==Black)
            {
                w->color=Red;
                x=x->parent;
            }
            else
            {
                if(w->left->color==Black)
                {
                    w->right->color=Black;
                    w->color=Red;
                    Left_Rotate(T,w);
                    w=x->parent->left;
                }
                else
                {
                    w->color=x->parent->color;
                    x->parent->color=Black;
                    w->right->color=Black;
                    Right_Rotate(T,x->parent);
                    x=T->Root;
                }
            }
        }
    }
    x->color=Black;
}

/*刪除紅黑樹節點的操作,一開始也是和普通二叉樹的刪除操作基本一樣,但也有三點不同
首先,在二叉樹中所有的NULL都換做了對Nil節點的引用
其次,不在需要判斷x是否爲空,直接將x的父節點連接在y上就行了,因爲x就算是Nil也是有完整結構的
最後,判斷如果刪除的y是黑色的,那麼調用修正方法*/
Node* RB_Delete(Tree* T,Node* z)
{
    Node* y;
    Node* x;
    if(z->left==&Nil||z->right==&Nil)
        y=z;
    else
        y=Tree_Successor(z);
    if(y->left!=&Nil)
        x=y->left;
    else
        x=y->right;
    x->parent=y->parent;
    if(y->parent==&Nil)
        T->Root=x;
    else if(y==y->parent->left)
        y->parent->left=x;
    else
        y->parent->right=x;
    if(y!=z)
    {
        z->data=y->data;
    }
    if(y->color==Black)
        RB_Delete_Fixup(T,x);
    return y;
}

/*順序統計數中用來取出第i小的元素
其實就是x的順序就保存在他的左孩子的節點上,所以每次和左孩子記錄的值進行比較
匹配成功,則直接返回
比左孩子小了,那麼在做孩子處遞歸調用算法
否則在右孩子處遞歸調用算法*/
Node* OS_Select(Node* x,int i)
{
    int r=x->left->Size+1;
    if(r==i)
        return x;
    else if(i<r)
        return OS_Select(x->left,i);
    else
        return OS_Select(x->right,i-r);
}

/*下面的算法給出一個指向節點x的指針,返回他的順序大小r
其實也是不斷的上移,節點x的左孩子的size+1表示他在以他爲根節點的子樹中的大小
如果x位於右子樹,那麼加上他的父節點的左子樹再加一,直到到達根加點*/
int OS_Rank(Tree* T,Node* x)
{
    int r=x->left->Size+1;
    Node* y=x;
    while(y!=T->Root)
    {
        if(y==y->parent->right)
            r=r+y->parent->left->Size+1;
        y=y->parent;
    }
    return r;
}

int main()
{
    Tree T;
    T.Root=&Nil;
    Node N1;N1.data=12;N1.left=N1.right=N1.parent=&Nil;
    Node N2;N2.data=5;N2.left=N2.right=N2.parent=&Nil;
    Node N3;N3.data=2;N3.left=N3.right=N3.parent=&Nil;
    Node N4;N4.data=9;N4.left=N4.right=N4.parent=&Nil;
    Node N5;N5.data=18;N5.left=N5.right=N5.parent=&Nil;
    Node N6;N6.data=15;N6.left=N6.right=N6.parent=&Nil;
    Node N7;N7.data=19;N7.left=N7.right=N7.parent=&Nil;
    Node N8;N8.data=17;N8.left=N8.right=N8.parent=&Nil;
    N1.Size=N2.Size=N3.Size=N4.Size=N5.Size=N6.Size=N7.Size=N8.Size=1;
    //Tree_Insert(&T,&N1);Tree_Insert(&T,&N2);Tree_Insert(&T,&N3);Tree_Insert(&T,&N4);
    //Tree_Insert(&T,&N5);Tree_Insert(&T,&N6);Tree_Insert(&T,&N7);Tree_Insert(&T,&N8);
    RB_Insert(&T,&N1);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N1.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N2);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N2.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N3);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N3.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N4);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N4.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N5);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N5.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N6);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N6.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N7);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N7.data,T.Root->data,T.Root->Size);
    RB_Insert(&T,&N8);
    printf("插入節點%d後,根節點爲%d,根節點size爲%d\n",N8.data,T.Root->data,T.Root->Size);
    Mid_Traverse(T.Root);
    printf("\n");
    Node* S=NULL;
    S=OS_Select(T.Root,6);
    printf("樹中第6小的元素爲%d\n",S->data);
    printf("樹中%d元素處於%d的位置\n",S->data,OS_Rank(&T,S));
    //Tree_Delete(T.Root,S);
    //Mid_Traverse(T.Root);
    return 0;
}


發佈了35 篇原創文章 · 獲贊 35 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章