红黑树(基本性质及插入操作)

二叉查找树中我们可以看到,一些基本的动态操作如查询,求后继、前驱,插入,删除等,其时间复杂度都与树的高度有关,当树的高度较低,也就是二叉树比较平衡的时候,这些操作执行的就会很快;但当树的高度较高时,例如每次向二叉树插入节点都一直向右,那么将会造成树的不平衡,这样的操作还不如直接使用链表方便。而为了保证树的平衡,这里就引入了红黑树的概念,他保证在最坏的情况下,树的高度总是log(n)。

1.红黑树的性质:

红黑树也是一棵二叉搜索树,只是在每个节点处添加了一个表示颜色的存储位,可以是红的或是黑的(为什么要有颜色,颜色的主要目的就是为了在保持二叉树稳定性操作时的有序性)。红黑树可以确保没有任何一条路径的长度是另一条的两倍(从后面的性质4可以推断),因而是接近平衡的。

树的每个节点包括5个域:color,key,left,right,p,这里需要引入叶子节点或是外节点的概念,这指的是树的最底层的虚拟出来的节点,在一般的二叉树中,最下边一个带有关键字的节点的左右孩子都是空的,而这时我们把这些空的节点当做叶子节点来对待,不属于叶子节点的称为内节点。

红黑树满足的性质:

1.每个节点要么是红的,要么是黑 的(这不是废话吗。。)。

2.根节点是黑的。

3.每个叶子节点是黑的。

4.如果一个节点是红的,那么它的两个孩子都是黑的(换句话说,其实就是只允许红-黑,或者黑-黑,不允许红-红的情况出现)。

5.对每个节点,从该节点到其子孙节点的所有路径上包含相同数目的黑节点(这是保证稳定性的关键性质)。

从某一个节点x出发(不包括x自身)到达一个叶子节点的任意一条路径上,黑色节点的数目称为黑高度(不要忘记叶子节点自己也是黑的),一个节点的黑高度是唯一的,因为由于性质5,从一个节点到他的子孙叶子节点的路过黑色节点的数目是一样的。根节点的黑高度定义为红黑树的黑高度。


对于一棵普通的搜索二叉树,如果其中有n个节点,那么其高度最大可以达到n,就是一直向右插入或者向左,而对于有n个节点(指的是内节点)红黑树来说,其高度最多为2log(n+1)。相比较与普通的二叉搜索树,红黑树可就平衡多了,这里有必要稍稍证明下这个定理。不想看的话可以直接跳过,没什么影响。

证明:

算法导论书上的证明方式是使用归纳法,这样比较严谨。而我这里决定不采用,而用一种更加直观的证明方式。

就拿上面那一副图举例,第一步,就是将所有的红节点和父节点合并,效果如下图(画的难看了点,将就下。。):


这样的结构称为2-3-4树。为什么叫这个名字?可以看到,这里每个节点只能有2个、3个或者4个孩子节点。它有一个不错的性质,就是所有的叶子节点都必然有相同的高度(原因可参见红黑树性质4、5,把红色节点向上归并后会发生什么?),也就是说,每个叶子节点的高度都等于根节点的黑高度。

假设这课树的高度为h',原红黑树的高度为h,对于两棵树来说,叶子节点的数量都是n+1(n为内节点的个数),这个不难理解。在2-3-4树中,由于每一个节点的数目都有2个、3个、4个这三种情况,据此对叶子节点加以约束:

2^h'<=n+1<=4^h'

化简之后可得到h'<=log(n+1),同时,别忘了一个性质,就是红色节点的数目最多是黑色节点数目的一半,而这里的2-3-4树就是用黑色节点吸收了红色节点,所以高度为原红黑树的至少为1/2,即h<=2h',加上之前的不等式可得:

h<=2log(n+1)

证明完毕。

这样是大体上较为直观的证明方法,比较严谨的方式还是要参考算法导论的归纳法证明。

2.旋转:

我们知道,红黑树以一棵比较平衡的二叉树,但在每次插入操作或者删除操作后,如果仅仅只是向之前的二叉搜索树一样的简单插入后,可能会导致二叉树的平衡被打破。为了在这些操作后仍旧能够保持二叉树的平衡,就需要对其进行一些修改,具体的方法就是改变某些节点的颜色以及指针结构。这里先将指针结构的修改。

指针结构的修改是通过旋转来完成的,具体可以分为左旋或者右旋操作。当在某个节点x上做左旋操作时,我们假设他的右孩子不是叶子节点,左旋以x到y之间的连线完成。它使y成为该子树新的根,x成为y的左孩子,而y的左孩子成为x的右孩子,具体如图:


下图显示了左旋的具体过程,右旋的话是对称的:


其他的一些操作如查询、返回最大值最小值都比较简单,直接对着之前的二叉搜索树就行了,这里重点介绍最难的更新部分:插入与删除。

3.插入操作

正如之前所提到的,每次插入一个新的节点都有可能破坏树的红黑性质,为了保持这种性质,需要用到三种不同的操作:

1.直接用到二叉搜索树的插入或者删除操作。

2.需要改变一些节点的颜色,尤其是刚刚插入的那个节点,需要给它上色。

3.对树既进行重新排序,改变指针结构(通过之前提到的旋转操作)。

下面举个例子,假设向红黑树中插入一个新的节点,首先需要给这个节点上色,一般情况下上的是红色(为什么是红的?投色子决定的。。),当然,按照二叉搜索树的操作的话,可以通过大小的比较插入到适当的位置,但是,插入完毕后,如果他的父节点的颜色同样是红色的话,那么就破坏了性质4,不能有两个红色的内节点相连,万幸的是,性质5仍旧成立,插入的红节点不会改变其中任何一个节点的黑高度。那么,该如何弥补性质4呢?

我们要把破坏的性质4向上移动,从插入的节点x开始,一直往根节点移动,注意下移动的颜色,这就需要重新上色。直到到达某一个点后,再用旋转操作,当然了,这可能需要再次重新上色。

假设我们需要往左下图的树中插入节点15:

-》

现在,违反了性质4,出现了两个红色节点相连的情况,如果简单的把它变成黑色的话,就会违反性质5,会打乱黑高度。唯一的弥补方法就是重新上色,但看看节点15的周围,第一感觉就是坑爹啊,没什么可以改变的。但如果再往上看,一直到15的爷爷节点10这里,可以看到节点10是黑色的,它的两个孩子节点都是红色的,这样是非常好的,因为我可以把节点10变成红色的,而两个孩子节点变成黑色的,这样的话这一块局部的问题就解决了,这样不会改变任何的黑高度,这样可以得到下左图。

但这样的话,节点10和节点18就会出现违例,像刚才那样,继续向上走,这样就到了节点10的爷爷节点7,但由于7的左孩子是黑色的,而右孩子是红色的,所以不能用刚才的直接换颜色的方法,而解决办法就是之前提到的旋转操作。下一个操作就是对18节点的右旋转。得到右下图。


虽然这里10和18之间还是存在违例现象,但我们将数弄的更加直了,这是我们想要的,它让18这个违例的点,与他的爷爷节点7形成了一条笔直的连线。接下来,我们将节点7与10的连线进行旋转,把节点7左旋,把节点10作为根节点。结果如左下图所示:

由于根节点必须为黑色的,所以做一点小小的修正,就得到了最终的图,如右下:


当然了,这样的过程不会是最终算法的严格执行步骤,不能够仅凭直觉来说明什么时候上色,什么时候旋转,这样的说明只是可以告诉你通过重新着色和旋转操作时能够保持红黑树的平衡的(前面的例子算是非常复杂的了,用到了多次着色和旋转操作)。

下面,我就会给出一个通用的算法:

1.将节点x插入到红黑树中

2.将x的颜色设置成红色

3.一直向上走,直到x变成根节点或者x变成黑色的

接下来,x的爷爷节点可能分为两种不同的形式如下图所示:


分为A、B两种情况,下面的直线表示的是无所谓是左孩子还是右孩子,以下以情况A为例子,B的话完全对称就行了。

在情况A中,还分为3中情况,下面逐一讨论。而我们讨论的对象是爷爷节点的另一个孩子节点,我们暂且把它叫做大叔节点(==),为什么那么在乎大叔节点呢?因为我么想知道我们能否做重新上色的步骤,而重新上色的思路是假设爷爷节点是黑色的,我们需要把爷爷节点的颜色向下转移到他的两个孩子节点,如果两个孩子节点都是红色的,那么直接就解决了,然后做的就是把问题向上移动了。

情形一:如果大叔节点是红色的,那么直接重新上色就可以了。

情形二:如果大叔节点是黑色的,且x节点是右孩子,那么需要进行一次左旋,进入情形三。

情形三:如果大叔节点是黑色的,且x节点是左孩子,那么对父节点和爷爷节点进行一次右旋,这样父节点就变成根节点了。

一般来说,进入情形三后,只需要对根节点重新上色就可以了。

总之,可以这么理解算法,一开始就是不停的向上重新上色(情形一),然后加入遇到情形二,那么做一次左旋,进入情形三,然后做一次右旋,左后重新上色,这就是算法的基本过程。



下面给出插入操作的代码,注意,这里的叶子节点不像二叉搜索树一样用NULL,而是采用一个叫做Nil的节点,这个节点与其他的叶子节点没什么区别,可以使用一个全局变量的Node节点,所有叶子节点全部指向这个节点。

下面的程序基于之前的二叉搜索树修改而来:

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

typedef enum Color{Red,Black}Color;

/*这里定义树的结构,每个节点为Node结构体,再加上一个头指针Tree*
在红黑树中,多了一个颜色的域Color,这里用枚举表示*/
typedef struct Node
{
    int data;
    Color color;
    struct Node* left;
    struct Node* right;
    struct Node* parent;
}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;
}

/*右旋操作,和左旋操作完全一样,代码是对称的*/
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;
}

/*以下是对红黑树插入之后的修正操作
下面的循环条件就是按照之前的那三种情形来实现的
首先,判读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=X->left;
        else
            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);
}

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;
    //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\n",N1.data,T.Root->data);
    RB_Insert(&T,&N2);
    printf("插入节点%d后,根节点为%d\n",N2.data,T.Root->data);
    RB_Insert(&T,&N3);
    printf("插入节点%d后,根节点为%d\n",N3.data,T.Root->data);
    RB_Insert(&T,&N4);
    printf("插入节点%d后,根节点为%d\n",N4.data,T.Root->data);
    RB_Insert(&T,&N5);
    printf("插入节点%d后,根节点为%d\n",N5.data,T.Root->data);
    RB_Insert(&T,&N6);
    printf("插入节点%d后,根节点为%d\n",N6.data,T.Root->data);
    RB_Insert(&T,&N7);
    printf("插入节点%d后,根节点为%d\n",N7.data,T.Root->data);
    RB_Insert(&T,&N8);
    printf("插入节点%d后,根节点为%d\n",N8.data,T.Root->data);
    Mid_Traverse(T.Root);
    printf("\n");
    Node* S=NULL;
    S=Tree_Search(T.Root,17);
    if(S!=NULL)
        printf("查找成功:%d\n",S->data);
    else
        printf("查找失败\n");
    Node* Min,*Max;
    Min=Tree_Minimum(T.Root);Max=Tree_Maximum(T.Root);
    printf("最小节点的值为:%d\n最大节点的值为:%d\n",Min->data,Max->data);
    Node* Su;
    Su=Tree_Successor(S);
    printf("%d的下一个元素是%d\n",S->data,Su->data);
    Su=Tree_Predecessor(S);
    printf("%d的上一个元素是%d\n",S->data,Su->data);
    //printf("删除一个元素:%d\n",S->data);
    //Tree_Delete(T.Root,S);
    //Mid_Traverse(T.Root);
    return 0;
}
这样,基本的插入操作就完成了,删除操作将在下一篇博客中讲到。



  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值