一、红黑树性质:
红黑树是具有以下性质的二叉查找树(左小右大):
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也为黑色