算法导论学习---红黑树具体解释之插入(C语言实现)

时间:2022-12-02 21:03:54

前面我们学习二叉搜索树的时候发如今一些情况下其高度不是非常均匀,甚至有时候会退化成一条长链,所以我们引用一些”平衡”的二叉搜索树。红黑树就是一种”平衡”的二叉搜索树,它通过在每一个结点附加颜色位和路径上的一些约束条件能够保证在最坏的情况下基本动态集合操作的时间复杂度为O(nlgn).以下会总结红黑树的性质,然后分析红黑树的插入操作,并给出一份完整代码。

先给出红黑树的结点定义:

#define RED 1
#define BLACK 0 ///红黑树结点定义,与普通的二叉树搜索树相比多了一个颜色域
typedef struct node
{
int key,color; ///定义1为红,0为黑
node *p,*left,*right;
node(){
color=BLACK; ///默认结点颜色为黑
}
}*RBTree;

一.从红黑树的性质讲起

红黑树是一棵二叉搜索树,它在每一个结点上增加了一个存储位来表示结点的颜色。能够是RED或BLACK。通过堆不论什么一条从根到叶子结点的简单路径上各个结点颜色进行约束,红黑树确保没有一条路径会比其他路径要长出两倍。因而是近似于平衡的。

以下给出红黑树的五条”红黑性质”。这几条性质是后面插入和删除的基础。

1).每一个结点或是红色或是黑色。

2).根结点是黑色的。

3).每一个叶结点是黑色的

4).假设一个结点是红色的。那么它的两个子节点都是黑色的。

5).对于每一个结点。从该结点出发到其全部后代叶结点的简单路径上均包括同样数目的黑结点。

我们设从某个结点x出发(不含该结点)到达一个叶结点的一条简单路径上的黑结点个数为该结点的黑高,记为bh(x)。显然依据性质5,每一个结点的黑高都是唯一的。以下我们来证明:一棵含有n个内部结点的红黑树的高度至多为2lg(n+1).

先证明以任一结点x为根的子树中至少包括2^bh(x)-1个内部结点。我们採用数学归纳法,对x的黑高进行归纳。

假设x的黑高高度为0即x为叶子结点,则显然正确。

x的黑高为bh(x),则其两棵子树的黑高为bh(x)或bh(x)-1(取决与它们本身的颜色),则可归纳出相应的内部结点至少为2^(bh(x)-1)-1,即x包括的内部结点至少为2^(bh(x)-1)-1+2^(bh(x)-1)-1+1=2^bh(x)-1,因此得证。然后如今我们设含n个结点的树高为h。然后依据性质4,能够知道从根到叶结点(不包括根结点)的任一条简单路径上都至少有一半的结点为黑色。因此树的黑高至少为h/2;然后有上面的结论得n>=2^(h/2)-1.解得h<=2lg(n+1)。

于是我们就证明了动态集合操作Search,Minimum,Successor等都能够在O(lgn)时间复杂度内完毕了。

以下给一张红黑树的图片。

算法导论学习---红黑树具体解释之插入(C语言实现)

二.旋转操作

后面的插入和删除过程中都会有破坏红黑树性质的情况发送,为了维护这些性质,必须要改变树中某些结点的颜色和指针结构。

指针结构的改动是通过旋转来完毕的。旋转被分为左旋和右旋;在插入和删除操作中这两个操作会多次出现。我们先来分析一下这两个操作。

这两个操作相应的图像例如以下:

算法导论学习---红黑树具体解释之插入(C语言实现)

从图像上能够看出,左旋和右旋操作并非非常复杂。以下我们以左旋为例进行解析:如图我们假设结点x的右儿子为y。左旋的前提是:结点必须要有右儿子。

左旋的结果是:y代替x,x成为y的左儿子。x成为y的左儿子。y的左儿子成为x的右儿子。

以下是具体的代码实现:

///左旋:对具有随意具有右孩子的结点能够进行
///传入要选择的结点指针的引用
///旋转以后x的右孩子y代替了x,而x成为y的左孩子,y的左孩子成为x的右孩子
///以下的代码就是完毕这三个部分
void LeftRotate(RBTree x)
{
RBTree y=x->right; ///y表示x的右孩子
x->right=y->left; ///第一步:将x的右孩子设为y的左孩子
if(y->left!=Nul)
y->left->p=x; y->p=x->p; ///更改y的父结点为x的父结点
if(x->p==Nul) ///第二步:y替代x,须要分情况讨论
rt=y; ///x原来是根结点,则设y为根结点
else if(x==x->p->left)
x->p->left=y; ///更改y为x父结点的左孩子
else
x->p->right=y; ///更改y为x父结点的右孩子 y->left=x; ///第三步:将x设为y的左孩子
x->p=y;
}

右旋与左旋是相似的,代码例如以下:

///右旋:对不论什么具有左孩子的结点能够进行
///传入要右旋的结点指针的引用
///旋转以后结点x被左孩子y替换,x成为y的右儿子,y的右孩子成为x的左孩子
void RightRotate(RBTree x)
{
RBTree y=x->left; ///y表示x的左孩子
x->left=y->right; ///第一步:x的左孩子更改为y的右孩子
if(y->right!=Nul)
y->right->p=x; y->p=x->p; ///更改y的父结点为x的父结点
if(x->p==Nul) ///第二步:y替代x,须要分情况讨论
rt=y; ///x原来为根结点,指定y为新根结点
else if(x==x->p->left)
x->p->left=y; ///更改x父结点的左孩子为y
else
x->p->right=y; ///更改x父结点的右孩子为y y->right=x; ///第三步:更改y的右结点为x
x->p=y;
}

三. 插入具体解释

理解红黑树插入和删除操作的难点在于:在我们改变一个结点以后,产生的情况太多。

相应的我们也就得到了主要的讨论方法:将全部的情况进行分类讨论,然后就能够理解代码为什么要这样写了。实际上仅仅要把《算法导论》上的那些Case理解清楚,红黑树的操作也就不难懂了。

以下应用分类讨论的思想来理解红黑树的插入操作。

首先以下我们先给出插入部分的代码:

///红黑树的插入
///RB插入函数与普通的BST的插入函数仅仅是略微有点不同
///我们将原来的null换成了Nul结点,然后对新增加的结点,染成红色
///然后调用RBInsertFixUp函数进行调整,使得红黑树的性质不被破坏
void RBInsert(int key)
{
RBTree z=new node;
z->color=RED;
z->key=key;
z->p=z->left=z->right=Nul;
RBTree y=Nul;
RBTree x=rt;
while(x!=Nul) ///依照二叉搜索树的性质寻找z的插入点
{
y=x;
if(z->key<x->key)
x=x->left;
else
x=x->right;
}
z->p=y;
if(y==Nul)///插入的是根节点
rt=z;
else if(z->key<y->key)
y->left=z;
else
y->right=z;
RBInsertFixUp(z); ///插入红色结点可能违反了红黑树的某些性质,调用调整函数进行调整
}

从代码上看,这里的插入和二叉搜索树的插入并没有什么太大的不同。仅仅是在最后加了一个插入调整函数:RBInsertFixUp()。之所以要增加调整函数是由于我们插入一个结点并将其染成红色导致红黑树的某条性质被违反了;所以我们须要着重的讨论究竟会违反那条性质,然后我们这么样在调整函数中就这样的情况进行性质的恢复。

首先我们设插入的结点为z。插入以后z会被染成红色。

假设z的父结点是黑色的。则不会违反不论什么性质。不须要调整。所以我们以下仅仅须要讨论z的父结点为红色的情况。

然后在这样的情况下,仅仅有性质4是肯定会被违反的。然后我们再按z的父结点是其祖父结点的左儿子还是右儿子分类(这两种情况没有本质性的差别,仅仅是在编码上略微有点不同而已)。以下我们仅仅讨论z的父结点是其祖父结点的左儿子的情况。然后我们设z的叔结点(即z父结点的兄弟结点)结点为y。在这些前提下我们再进行分类讨论:

1)情况1:y的颜色为红色

这时其祖父结点一定是黑色的(由于在插入这个结点之前红黑树没有性质被违反)。

这样的情况下,我们能够将z的父结点染成黑色。z的叔结点染成黑色,z的祖父结点染成红色;这样染色以后。z的祖父结点以下的子树肯定是合法的红黑树,可是z的祖父结点可能违反性质4了。相当于将z在树中上升了两层(z=z->p->p)。

这样的情况下假设新z的父结点为黑就会退出循环了(假设新z为根节点就一定会退出。这时候根节点有可能会被染成红色,所以在退出循环后须要再将根节点染成黑色)。

这样的情况的示意图例如以下:

算法导论学习---红黑树具体解释之插入(C语言实现)

2):y的颜色为黑

在y的颜色为黑的情况下我们在按z是其父结点的左孩子还是右孩子进行分类

(1).z是其父结点的左孩子

这时我们能够通过将z的父结点染成黑色,z的祖父结点染成红色。然后对z的祖父结点进行一次右旋恢复性质4,而且不违反其他的性质(对于这一点我们仅仅要自己画一下图就能够非常清楚的看到了)。

(2).z是其父结点的右孩子

这样的情况显然是能够通过将z指向z的父结点,然后对z进行一次左旋就能够变成情况(1)处理了。

这两种情况的示意图例如以下:当中图片上的情况二相应我们这里的(2),情况三即为我们这里的(1)

算法导论学习---红黑树具体解释之插入(C语言实现)

总的来讲仅仅要y为黑,我们就能够退出循环了。上面的两种情况的划分只是是内部结构的一些小转变而已。

综上。事实上红黑树的插入操作主要是违反了性质4。调整函数的过程就是分了两类情况对性质4进行调整的过程。并非非常复杂。上面给出的是z的父结点为其祖父结点左儿子的情况。事实上对于右儿子的情况基本上是一样的(具体的见代码)。

另一个令人困惑的地方在于红黑树的代码中大量的使用了z->p->p这样的形式,可是这真的不会导致空指针异常吗?对此算法导论上进行了比較具体的论证。

在这里我简单的说一下我的看法:首先,在插入根节点的时候,是不会进入循环中的,所以就不会引用z->p->p。然后在插入根节点以后。除根结点外每一个结点的祖父结点都是一定存在的,所以第一次引用不会出问题。问题仅仅有可能出如今z在树中上移的情况下(相应情况1),可能会上移到某个位置。而这个位置的父结点不存在父结点,这时就会导致空指针的危急了;可是我们要注意到,这个位置仅仅有可能是根结点,而假设z移动到了根节点,那么就会由于根节点的父结点是黑色而退出循环了!根本就不会再出现z->p->p这样的形式的引用。所以不须要操心z->p->p的引用会出现空指针异常。

以下给出红黑的的插入调整函数RBInsertFixUp()函数的代码,其相应我们上面讨论的三种情况:


///红黑树插入调整函数
///我们将插入结点染成红色,可能违反了性质4,所以要进行调整
///调整的过程事实上就是依据不同的情况进行分类讨论,不断转换的过程
///最后转成能够通过染色和旋转恢复性质的情况
void RBInsertFixUp(RBTree z)
{
///在以下的代码中z结点总是违反性质4的那个结点
while(z->p->color==RED) ///x是红色,它的父结点也是红色就说明性质4被违反,要持续调整
{
///以下的过程按x->p是其祖父结点的左孩子还是右儿子进行分类讨论
if(z->p==z->p->p->left) ///父结点是其祖父结点的左孩子
{
RBTree y=z->p->p->right; ///表示z的叔结点 ///以下按y的颜色进行分类讨论
if(y->color==RED)
{///假设y是红色并z的祖父结点一定是黑色的,这时我们通过以下的染色过程
///在保证黑高不变的情况下(性质5),将z在树中上移两层,z=z->p->p
z->p->color=BLACK;
y->color=BLACK;
z->p->p->color=RED;
z=z->p->p;///假设上移到根节点或某个父结点不为红的结点就能够结束循环了
}
else ///叔结点为黑色
{ ///此时要依据z是其父结点的左孩子还是右孩子进行分类讨论
///假设z是左孩子则能够直接能够通过染色和右旋来恢复性质
///假设z是右孩子则能够先左旋来转成右孩子的情况 if(z==z->p->right)
{
z=z->p;
LeftRotate(z); ///直接左旋
}
///又一次染色,再右旋就能够恢复性质
z->p->color=BLACK;
z->p->p->color=RED;
RightRotate(z->p->p);
}
}
else///父结点是祖父结点的右孩子
{
RBTree y=z->p->p->left; ///叔结点
if(y->color==RED)
{
z->p->color=BLACK;
y->color=BLACK;
z->p->p->color=RED;
z=z->p->p;
}
else
{///右儿子的时候能够直接左旋,又一次调色恢复性质
///左儿子能够先右旋成右儿子再处理
if(z==z->p->left)
{
z=z->p;
RightRotate(z);
}
z->p->color=BLACK;
z->p->p->color=RED;
LeftRotate(z->p->p);
}
}
}
///将根节点染成黑色,是必要的步骤。处理两种情况
///1.第一次插入根结点被染成红色的情况
///2.和在上面的循环中根节点可能被染成红色的情况
rt->color=BLACK;
}

下一篇:算法导论学习–红黑树具体解释之删除