一、红黑树性质:
红黑树是具有以下性质的二叉查找树(左小右大):
1.每一个节点或者是红色或者是黑色
2.根是黑色
3.如果一个节点是红的,那么它的子节点必须的黑的
4.从一个节点到一个NULL指针的每一条路径必须包含相同数目的黑色节点。
红黑树的高度最多是2log(N+1),平均红黑树的深度和AVL树一样,(所以查找时间一般接近最优)红黑树的优点是执行插入操作所需要的开销相对较低,在实践中发生旋转相对较少。
困难的操作是插入和删除,需要通过颜色的改变和树的旋转来满足以上4条性质
二、红黑树的插入:
分情况讨论:
1、如果插入节点的父节点是黑色的,那么插入操作很容易完成,直接进行插入。
2、如果插入节点的父节点是红色的,【因为性质4.所以我们插入的节点一定是红色的,那么这时,我们需要进行颜色翻转和位置旋转来恢复性质】
插入红色节点,由于性质3,我们必须进行颜色翻转和位置旋转来完成插入,保持红黑树的性质
分两种情况:
一字型:
之字形:
我们对以下红黑树进行插入:
但是当插入45时上述的方法不行了,经过旋转后有两个连续的红节点。我们需要新的方法
这时,我们想到将红色节点颜色与黑色节点颜色进行翻转。因此又分为两种情况:
1、插入节点的父节点的兄弟节点是黑色的,则利用上述一字型、之字形规则颜色翻转和位置旋转直接恢复
2、插入节点的父节点的兄弟节点是红色的(即一个节点右两个红儿子),则需要将拥有两个红色儿子的节点与父节点颜色翻转,然后再进行插入
应用上述规则,我们重新插入45。
a、我们需要先对拥有两个红儿子的节点进行颜色翻转,然后再进行插入。
b、经过翻转后,出现一字型情形,我们进行一字型旋转。
c、经过一字型旋转,现在就可以直接插入节点45
因此我们得到红黑树插入操作的步骤:
1、在从根节点向插入节点,有两个红色儿子的节点则需要进行颜色翻转(下滤)
2、翻转完后若不满足红黑树性质,则进行旋转操作。
3、插入
三、程序实现:
#include "redblack.h"
#include <stdlib.h>
#include "fatal.h"
typedef enum ColorType { Red, Black } ColorType;
struct RedBlackNode
{
ElementType Element;
RedBlackTree Left;
RedBlackTree Right;
ColorType Color;
};
static Position NullNode = NULL;
RedBlackTree
Initialize( void )
{
RedBlackTree T;
if( NullNode == NULL )
{
NullNode = malloc( sizeof( struct RedBlackNode ) );
if( NullNode == NULL )
FatalError( "Out of space!!!" );
NullNode->Left = NullNode->Right = NullNode;
NullNode->Color = Black;
NullNode->Element = 12345;
}
T = malloc( sizeof( struct RedBlackNode ) );
if( T == NULL )
FatalError( "Out of space!!!" );
T->Element = NegInfinity;
T->Left = T->Right = NullNode;
T->Color = Black;
return T;
}
void
Output( ElementType Element )
{
printf( "%d\n", Element );
}
static void
DoPrint( RedBlackTree T )
{
if( T != NullNode )
{
DoPrint( T->Left );
Output( T->Element );
DoPrint( T->Right );
}
}
void
PrintTree( RedBlackTree T )
{
DoPrint( T->Right );
}
static RedBlackTree
MakeEmptyRec( RedBlackTree T )
{
if( T != NullNode )
{
MakeEmptyRec( T->Left );
MakeEmptyRec( T->Right );
free( T );
}
return NullNode;
}
RedBlackTree
MakeEmpty( RedBlackTree T )
{
T->Right = MakeEmptyRec( T->Right );
return T;
}
Position
Find( ElementType X, RedBlackTree T )
{
if( T == NullNode )
return NullNode;
if( X < T->Element )
return Find( X, T->Left );
else
if( X > T->Element )
return Find( X, T->Right );
else
return T;
}
Position
FindMin( RedBlackTree T )
{
T = T->Right;//
while( T->Left != NullNode )
T = T->Left;
return T;
}
Position
FindMax( RedBlackTree T )
{
while( T->Right != NullNode )
T = T->Right;
return T;
}
static Position
SingleRotateWithLeft( Position K2 )
{
Position K1;
K1 = K2->Left;
K2->Left = K1->Right;
K1->Right = K2;
return K1; /* New root */
}
static Position
SingleRotateWithRight( Position K1 )
{
Position K2;
K2 = K1->Right;
K1->Right = K2->Left;
K2->Left = K1;
return K2; /* New root */
}
static Position
Rotate( ElementType Item, Position Parent )//旋转
{
if( Item < Parent->Element )//判断是左旋转还是右旋转
return Parent->Left = Item < Parent->Left->Element ?
SingleRotateWithLeft( Parent->Left ) :
SingleRotateWithRight( Parent->Left );
else //判断是左旋转还是右旋转
return Parent->Right = Item < Parent->Right->Element ?
SingleRotateWithLeft( Parent->Right ) :
SingleRotateWithRight( Parent->Right );
}
static Position X, P, GP, GGP;//静态全局变量,保存节点X,父节点,祖父节点,曾祖父节点
static
void HandleReorient( ElementType Item, RedBlackTree T )//主要进行颜色翻转,并调用旋转函数
{
X->Color = Red; //翻转X与其儿子节点的颜色
X->Left->Color = Black;
X->Right->Color = Black;
//X翻转完后颜色是红色
if( P->Color == Red ) //如果父节点是红色的
{
GP->Color = Red;
if( (Item < GP->Element) != (Item < P->Element) )//判断是否需要多一次旋转(之字形双旋转)
P = Rotate( Item, GP );
X = Rotate( Item, GGP );
X->Color = Black;
}
T->Right->Color = Black; /* Make root black */
}
RedBlackTree
Insert( ElementType Item, RedBlackTree T )
{
X = P = GP = T;
NullNode->Element = Item;//这样初始化,当X下滤到叶子节点时,下面的while循环就会跳出
while( X->Element != Item ) //进行下滤
{
GGP = GP; GP = P; P = X;
if( Item < X->Element )
X = X->Left;
else
X = X->Right;
if( X->Left->Color == Red && X->Right->Color == Red )//如果X有两个红儿子节点,就进行颜色翻转,位置旋转
HandleReorient( Item, T );
}
if( X != NullNode )//说明没有下滤到叶子节点,上述while循环就跳出了,所以Item已经在树中了,不用插入,返回NULL
return NullNode;
//将Item进行插入
X = malloc( sizeof( struct RedBlackNode ) );
if( X == NULL )
FatalError( "Out of space!!!" );
X->Element = Item;//执行到这一步说明树中没有Item,X已经等于NullNode,X->Color=Black;
X->Left = X->Right = NullNode;
if( Item < P->Element ) //将Item插入合适的位置
P->Left = X;
else
P->Right = X;
HandleReorient( Item, T ); //因为之前X的Color为黑色,所以这里将颜色变为红色
return T;
}
RedBlackTree
Remove( ElementType Item, RedBlackTree T )
{
printf( "Remove is unimplemented\n" );
if( Item )
return T;
return T;
}
ElementType
Retrieve( Position P )
{
return P->Element;
}
完整程序代码:GitHub
红黑树的具体实现比较复杂,我们使用两个标记节点:一个是根,一个是NullNode,它的作用像是在伸展树中那样是指示一个Null指针。
根标记将存储关键字-oo和一个指向真正的根的右指针。
四、插入程序执行过程分析:
插入元素45:
X=P=GP=T=-10000
进入while循环
1、
GGP=GP=-10000; GP=P=-10000; P=X=-10000;
Item=45>-10000
X=X->Right=30;
X->Left->Color == Red && X->Right->Color == Red 不成立
2、
GGP=GP=-10000;GP=P=-10000;P=X=30;
Item=45>30
X=X->Right=70;
X->Left->Color == Red && X->Right->Color == Red 不成立
3、
GGP=GP=-10000;GP=P=30;P=X=70;
Item=45<70
X=X->Left=60;
X->Left->Color == Red && X->Right->Color == Red 不成立
4、
GGP=GP=30;GP=P=70;P=X=60;
Item=45<60
X=X->Left=50;
X->Left->Color == Red && X->Right->Color == Red 成立
HandleReorient( 45, T );
将X与其儿子节点的颜色翻转互换,X翻转完后颜色是红色。
P=60颜色也是红色,所以需要进行旋转变色
根据上面旋转示意图可知,不论单旋转还是双旋转,都需要将G的颜色变为红色。
一字型,进行单旋转。
X = Rotate( 45, 30 );
因为
Item > Parent->Element
45>30
Item < Parent->Right->Element
45<70
所以
SingleRotateWithLeft( Parent->Right )
SingleRotateWithLeft( 70 )
K2=70
K1=60
K2->Left=65
K1->Right=70
因此Parent->Right=60
Rotate( 45, 30 )返回60
X=60;
最后再将X的颜色变为黑色。
T->Right->Color = Black并确保根的颜色为黑色
5、
GGP=GP=70;GP=P=60;P=X=60;
Item=45<60
X=X->Left=50;
X->Left->Color == Red && X->Right->Color == Red 不成立
6、
GGP=GP=60;GP=P=60;P=X=50;
Item=45<50
X=X->Left=40;
X->Left->Color == Red && X->Right->Color == Red 不成立
7、
GGP=GP=60;GP=P=50;P=X=40;
Item=45>40
X=X->Right=NullNode;
X->Left->Color == Red && X->Right->Color == Red 不成立
8、跳出循环
Item=45 > P->Element=40
P->Right=X=45
HandleReorient( 45, T )
将X颜色变为红色,X子节点的颜色变为黑色
P->Color为黑色
T->Right->Color也为黑色