C语言自顶向下实现红黑树
一、红黑树的介绍
红黑树,一种自平衡的二叉查找树,在每一个结点内部储存节点的颜色信息(红色或者黑色),能够以O(logN)的时间复杂度查找、插入和删除。
红黑树是一种二叉查找树,满足二叉树的一般性质:
- 对于二叉树的任意一个节点,如果左子树非空,左子树的所有节点的键值均小于该结点;如果右子树非空,右子树所有节点的键值均大于该结点。
- 树中不存在键值相等的节点。
红黑树虽然在本质上是一棵二叉查找树,但其着色的特性使得红黑树保持了高度平衡性,其高度最高是2log(N+1)。红黑树着色性质如下:
- 每一个节点或为黑色,或为红色。
- 根是黑色的。
- 如果一个节点是红色的,那么它的子节点必为黑色。
- 从一个节点到一个NULL指针的每一条路径必须包含相同数目的黑色节点。
如图显示一棵红黑树满足以上性质:
(注:每个叶子节点的两个NULL指针视为黑色)
二、红黑树的旋转
红黑树所需要用到的旋转与二叉查找树相同,分为左旋转和右旋转。
设父亲结点为K2,要旋转的儿子结点为K1。
左旋转:K2变为K1的左儿子,K1的左儿子变为K2的右儿子。
实现代码如下:
static pRedBlackTree SingleRotateWithRight(pRedBlackTree k2) {
//右侧子树单旋转
pRedBlackTree k1;
k1 = k2->pRight;
k2->pRight = k1->pLeft;
k1->pLeft = k2;
return k1;
}
右旋转:K2变为K1的右儿子,K1的右儿子变为K2的左儿子。
实现代码如下:
static pRedBlackTree SingleRotateWithLeft(pRedBlackTree k2) {
//左侧子树单旋转
pRedBlackTree k1;
k1 = k2->pLeft;
k2->pLeft = k1->pRight;
k1->pRight = k2;
return k1;
}
三、插入删除前的准备
为了方便插入节点,将根不指向NULL指针,而是指向一个表示空的NullNode节点。同时,这个根不是真正树的根,我将树真正的根表示为假根T->pRight(打印树等情况下传递T->pRight)。
#define ElementType int
#define ColorType char
#define Black 'B'
#define Red 'R'
#define Infinity 999999
typedef struct RedBlackTree {
ElementType Element;
struct RedBlackTree *pLeft;
struct RedBlackTree *pRight;
ColorType Color;
}RedBlackNode, *pRedBlackTree;
pRedBlackTree NullNode = NULL;
pRedBlackTree Initialize(void) {
pRedBlackTree T;
if (NullNode == NULL) {
NullNode = (pRedBlackTree)malloc(sizeof(RedBlackNode));
if (NullNode == NULL) {
printf("SpaceNotAvailable");
exit(0);
}
NullNode->pLeft = NullNode->pRight = NullNode;
NullNode->Color = Black;
NullNode->Element = Infinity;
}
//创建头节点
T = (pRedBlackTree)malloc(sizeof(RedBlackNode));
if (T == NULL) {
printf("SpaceNotAvailable");
exit(0);
}
T->Element = -Infinity;
T->pLeft = T->pRight = NullNode;
T->Color = Black;
return T;
}
四、自顶向下红黑树插入节点
相比于自底向上的写法,自顶向下的红黑树不需要保存父节点,同时可以避免处理X(当前节点)的兄弟为红色的情况,具体操作解释如下:
Step1:设置全局静态变量X(当前节点)、P(父节点)、GP(祖父节点)、GGP(曾祖父节点),设置这些变量的原因是在旋转的过程中需要将旋转过后的子树重新链接到GP上。
Step2:当X的两个儿子均为红色的时候,将两个儿子染为黑色,X染为红色。
注意:此时有可能出现X和X的父节点均为红色的情况,此时违背“红黑树红色节点的子节点必为黑色”的性质,需要进行一系列的旋转来纠正。(若满足此类情况执行Step3,不满足直接执行Step4)
Step3:此时的情况是X和P均为红色,有两种情况分别有不同的处理方式。现只讲解下潜方向为左子树方向的情况,向右子树下潜只是为向左子树下潜的镜像情况,与此类似。
操作会平衡左右子树的黑色节点数,使其与处理前数目相同。
X、P、GP呈一字型:P节点的颜色变为黑色,GP变为红色,右旋转GP节点,使P成为新根节点,GP成为P的右儿子。
GP的右儿子S不可能为红色,因为P为红色,此时S为红色,则其在Step1已经被去除。
X、P、GP呈之字形:X变为黑色,GP变为红色。先进行P的左旋转,使X、P、GP变为一字型,再进行G的右旋转。
处理结束后转向Step4。
Step4:继续向下前进,如果查找到有相同键值的节点,直接退出;达到NULL节点,进入第5步;否则,重复Step2-4步。
Step5:由于Step4中没有退出,此时已经到达要插入的位置,插入节点,并设节点为红色。
此时有两种情况:
第一种:插入节点的父亲节点为黑色,插入完成。
第二种:插入节点的父亲节点为红色,连续两个节点为红色,需要进行一次Step3来调整节点的颜色和位置,插入完成。
插入的完整代码如下:
static pRedBlackTree X, P, GP, GGP;//用于储存当前节点、父节点、祖父节点、曾祖父节点
static pRedBlackTree Rotate(ElementType Item, pRedBlackTree Parent) {
//进行在节点X处的旋转,Item用于检查儿子
if (Item < Parent->Element)
return Parent->pLeft = Item < Parent->pLeft->Element ?
SingleRotateWithLeft(Parent->pLeft) :
SingleRotateWithRight(Parent->pLeft);
else
return Parent->pRight = Item < Parent->pRight->Element ?
SingleRotateWithLeft(Parent->pRight) :
SingleRotateWithRight(Parent->pRight);
}
static void HandleReorient(ElementType Item, pRedBlackTree T) {
X->Color = Red;
X->pLeft->Color = X->pRight->Color = Black;
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->pRight->Color = Black;//无论如何根节点均为黑色
}
pRedBlackTree Insert(ElementType Item, pRedBlackTree T) {
X = P = GP = T;
NullNode->Element = Item;
while (X->Element != Item) {//从顶向下
GGP = GP; GP = P; P = X;
if (Item < X->Element)
X = X->pLeft;
else
X = X->pRight;
if (X->pLeft->Color == Red && X->pRight->Color == Red)//两个子节点均为红色,将X变为红色,子节点为黑色
HandleReorient(Item, T);
}
if (X != NullNode)
return NullNode;//节点已经存在
X = (pRedBlackTree)malloc(sizeof(RedBlackNode));
if (X == NULL) {
printf("SpaceNotAvailable");
exit(0);
}
X->Element = Item;
X->pLeft = X->pRight = NullNode;
if (Item < P->Element)//链接到父节点
P->pLeft = X;
else
P->pRight = X;
HandleReorient(Item, X);
return T;
}
五、自顶向下红黑树删除节点
红黑树自顶向下删除模式都在最终归结于删除了一片树叶。当查找到要删除的节点时,用该结点的最大或最小的子节点删除(决定于从左子树还是右子树出发);若该结点只有一个儿子,进行另外的操作解决,以免出现两个连续红色节点的情况。
如果找到的树叶为红色,则可以直接删除而不破坏红黑树的性质;如果树叶为黑色,删除操作会使一些节点到NULL指针的路径上黑色节点数目减少,违背红黑树的性质。
可以通过保证从上到下删除期间树叶都是红色的来解决。
删除的思路:
第一种:此节点左右子树齐全,查找右子树的最小节点来代替该节点,并删除此最小节点。
第二种:此节点只存在右子树,删除方式与第一种相同。
第三种:此节点只存在左子树,查找左子树的最大节点来代替该节点,并删除此最大节点。
以下是如何保证删除期间树叶为红色办法:
Step1:定义当前节点X、父节点P、祖父节点GP、兄弟节点T。
Step2:此时X、T为黑色,P为红色。
分为两种情况,一种是X有两个黑色的非空节点(不存在一个为空的情况,因为这样就会使节点到达NULL指针的路径上经过的黑色节点数不等),设为Step2A;另一种是X至少有一个红色的儿子,设为Step2B。两种情况的处理方式不同,每一次处理完回到Step2,直到X指向NULL时退出。
Step2A:可以划分为三种情况。
Step2A1:T有两个黑色的儿子。
此时的操作不需要经过任何旋转,只需要将X、T变为红色,P变为黑色即可。经过此操作后当前节点为红色。
Step2A2:T的左儿子为红色。
此时将X变为红色,P变为黑色。先对P进行左旋转,再对T进行右旋转。这样操作之后没有改变左右路径的黑色节点数,并且当前节点的颜色已经变为红色(此步处理完成后下潜要么找到要删除的节点,要么回到Step2A1)。
Step2A3:T的右儿子为红色的,或者两个儿子均为红色。
此时对X、P、T、R全部反转颜色,并将P节点进行一次左旋转。
在每一次操作结束后查看X是不是叶子节点,是的话就将X删除;不是就查找子树中的节点来代替X,并删除子树中对应的节点。
Step2B:X至少有一个儿子不为黑色。
在开始之前我们需要判断X是否为要删除的节点,如果不是则有两种可能继续操作。
此时的状态是P为红色,X、T均为黑色。
Step2B1:X的左儿子为红色。
此时X已经判断过不为要删除的节点,左儿子为红色不影响红黑树的性质,因而直接下潜即可。
Step2B2:此时X的右儿子为红色,下潜落到黑色节点上。
如果X在没有下潜之前为要删除的值,那么X存在红色的儿子必定不为叶子节点,因此与上文删除方式相同,即查找最大、最小节点代替该结点。
否则,X下潜,P、T变色,对P进行左旋转,此时回到Step2的初始情况。
删除代码如下:
注:以下代码中Now表示X,Bro表示Now的兄弟,Father表示P,GFather表示GP,此注释用以和插入的讲解相同,以免造成误解。
static pRedBlackTree Now, Bro, Father, GFather, GGFather;
static void SolveStep2A23(void) {
if (Father->pLeft == Now) {//Bro在右边
if (Bro->pLeft->Color == Red) {//step2A2
Father->Color = Black;
Now->Color = Red;
Father->pRight = SingleRotateWithLeft(Bro);
if (GFather->pLeft == Father)
GFather->pLeft = SingleRotateWithRight(Father);
else
GFather->pRight = SingleRotateWithRight(Father);
}
else {//step2A3,Now、Father、Bro、Bro->pRight全部变色,对Father进行左旋转
Now->Color = Red;
Father->Color = Black;
Bro->Color = Red;
Bro->pRight->Color = Black;
if (GFather->pRight == Father)
GFather->pRight = SingleRotateWithRight(Father);
else
GFather->pLeft = SingleRotateWithRight(Father);
}
}
else {//Bro在左边
if (Bro->pRight->Color == Red) {//step2A2
Now->Color = Red;
Father->Color = Black;
Father->pLeft = SingleRotateWithRight(Bro);
if (GFather->pLeft == Father)
GFather->pLeft = SingleRotateWithLeft(Father);
else
GFather->pRight = SingleRotateWithLeft(Father);
}
else {//step2A3
Now->Color = Red;
Father->Color = Black;
Bro->Color = Red;
Bro->pLeft->Color = Black;
if (GFather->pRight == Father)
GFather->pRight = SingleRotateWithLeft(Father);
else
GFather->pLeft = SingleRotateWithLeft(Father);
}
}
}
static pRedBlackTree DeleteNode(pRedBlackTree Target, pRedBlackTree T) {
//删除Target指向的节点
pRedBlackTree Origin = T, Par, tmp;
while (T != Target) {
Par = T;
if (Target->Element < T->Element)
T = T->pLeft;
else
T = T->pRight;
}
if (T == Origin) {
if (T->pRight != NullNode)
tmp = T->pRight;
else
tmp = T->pLeft;
free(T);
return tmp;
}
if (Par->pRight == T) {
if (T->pRight != NullNode)
Par->pRight = T->pRight;
else
Par->pRight = T->pLeft;
}
else {
if (T->pRight != NullNode)
Par->pLeft = T->pRight;
else
Par->pLeft = T->pLeft;
}
free(T);
return Origin;
}
static ElementType FindItem(ElementType Item) {
ElementType tmp = 0;
pRedBlackTree ToDelete = NULL;
//如果是要找的节点,先判断是否是叶子,不是再通过替换节点删除
if (Now->pLeft == NullNode && Now->pRight == NullNode) {//证明Now是叶子
if (Father->pRight == Now)
Father->pRight = NullNode;
else
Father->pLeft = NullNode;
free(Now);
Now = NullNode;
tmp = Item;
}
else {//此时该节点不是叶子
if (Now->pRight != NullNode) {//从右边查找最小节点放在当前节点上
ToDelete = Find(Item, Now->pRight);
Now->Element = ToDelete->Element;
tmp = ToDelete->Element;
if (ToDelete->Color == Red) {//如果找到的节点为红色,可以直接删除
Now->pRight = DeleteNode(ToDelete, Now->pRight);
Now = NullNode;//设为NullNode用于直接退出Removw操作
}
else {/*A2中:往右前进,此时新的Now,(之前的Now是红色)一定是黑色,Bro也一定是黑色,此时就回到了step2
B2中:右前进,此时新的Now,可能是红色也可能是黑色,如果是红色的话,一定不是ToDelete节点*/
GFather = Father; Father = Now; Bro = Father->pLeft;
Now = Now->pRight;
}
}
else {//从左边查找最大节点
ToDelete = Find(Item, Now->pLeft);
Now->Element = ToDelete->Element;
tmp = ToDelete->Element;
if (ToDelete->Color == Red) {
Now->pLeft = DeleteNode(ToDelete, Now->pLeft);
Now = NullNode;
}
else {
GFather = Father; Father = Now; Bro = Father->pRight;
Now = Now->pLeft;
}
}
}
return tmp;
}
static void NormalDown(ElementType Item) {
if(Item < Now->Element) {
GFather = Father; Father = Now; Bro = Father->pRight;
Now = Now->pLeft;
}
else {
GFather = Father; Father = Now; Bro = Father->pLeft;
Now = Now->pRight;
}
}
static void Solve2B(void) {
Bro->Color = Black;
Father->Color = Red;//保证路径上黑色节点数相同
if(Father->pLeft == Now) {
if(GFather->pLeft == Father)
GFather->pLeft = SingleRotateWithRight(Father);
else
GFather->pRight = SingleRotateWithRight(Father);
Bro = Father->pRight;
}
else {
if(GFather->pLeft == Father)
GFather->pLeft = SingleRotateWithLeft(Father);
else
GFather->pRight = SingleRotateWithLeft(Father);
Bro = Father->pLeft;
}
}
pRedBlackTree Remove(ElementType Item, pRedBlackTree T) {
Now = T->pRight;
Bro = T->pLeft;
GGFather = GFather = Father = T;
while (Now != NullNode) {
if (Now->pLeft->Color == Black && Now->pRight->Color == Black) {//Step2,A型处理情况,X有两个黑儿子
if (Bro->pLeft->Color == Black && Bro->pRight->Color == Black) {
//兄弟节点为两个黑儿子的情况,将Bro和Now改为红色,Father改为黑色
Father->Color = Black;
Now->Color = Red;
if (Bro != NullNode)
Bro->Color = Red;
}
else
SolveStep2A23();//Bro有一个儿子不为黑色,实行之字形旋转或者一字型旋转
if (Now->Element == Item)
Item = FindItem(Item);//Now是否为NullNode来完成前进或者结束删除
else
NormalDown(Item);//没有找到节点时前进,进入Step2
}
else {//B型处理情况,X至少有一个儿子不是黑的
if (Now->Element != Item)
NormalDown(Item);
else
Item = FindItem(Item);
if (Now == NullNode)
break;//完成了删除,Now变为了NullNode
//如果没有完成删除,则节点已经下降
if (Now->Color == Black)
Solve2B();/*此时兄弟节点必为红色,旋转Now、Bro变为黑色,Father变为红色,
回到step2,查看X是否为要查找的节点*/
else if (Now->Element != Item)
NormalDown(Item);//红色继续下降,Now和Bro都会为黑色
else
Item = FindItem(Item);
}
}
T->Color = Black;
T->pRight->Color = Black;
return T;
}
参考书籍:《数据结构与算法分析–C语言描述》