在之前的随机选择算法中,我们可以很快的在集合中寻找到第i小的元素,然而,这样的集合并不支持动态的扩充。这一节里,将介绍通过红黑树(具体可参考红黑树1,红黑树2两篇文章)的扩充,使得任意的顺序统计量都可以在短时间内查找到,而这样的数据结构同时也支持数据的更新。
这样的数据结构称为顺序统计树,如下图。树的节点大致上与红黑树的类似,但增加了一个记录子树大小的域size[x],定义哨兵Nil的子树大小为0,即size[Nil[T]]=0。有等式size[x]=size[left[x]]+size[right[x]]+1
顺序统计树主要有两种算法:
1.检索具有给定排序的元素
其实就是x的顺序就保存在他的左孩子的节点上,所以每次和左孩子记录的值进行比较,匹配成功,则直接返回;比左孩子小了,那么在做孩子处递归调用算法;否则在右孩子处递归调用算法。
2.确定一个元素的秩
和上一个差不多,只是传入的节点,返回的节点所处的位置。
所有节点在插入的时候时需要注意,因为插入的过程就是和某一个节点比较大小,如果小于这个节点,那么就往左孩子处递归调用算法,否则的话向右孩子处递归。每次递归的时候,都需要给当前节点的size位加上1。一直递归到底层,这个时候,由于红黑树的插入性质,需要做左旋或者右旋的一些操作(颜色的改变不会影响到子树的大小),旋转之后需要重新确定好节点的大小。
下面给出代码(基于之前的红黑树的,所以嘛有点长==):
PS:其实改动的代码不到100行,其他的就是红黑树的源代码
#include <stdio.h>
#include <stdlib.h>
typedef enum Color{Red,Black}Color;
/*这里定义树的结构,每个节点为Node结构体,再加上一个头指针Tree*
在红黑树中,多了一个颜色的域Color,这里用枚举表示
顺序统计数中,多了一个表示子树大小的Size*/
typedef struct Node
{
int data;
Color color;
struct Node* left;
struct Node* right;
struct Node* parent;
int Size;
}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;
Y->Size=X->Size;
X->Size=X->left->Size+X->right->Size+1;
}
/*右旋操作,和左旋操作完全一样,代码是对称的*/
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;
X->Size=Y->Size;
Y->Size=X->left->Size+X->right->Size+1;
}
/*以下是对红黑树插入之后的修正操作
下面的循环条件就是按照之前的那三种情形来实现的
首先,判读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->Size++;
X=X->left;
}
else
{
X->Size++;
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);
}
/*传递的节点x有两种情况,在y被删除之前,如果y有个不是哨兵Nil的节点,那么x就是y的唯一的孩子
如果y没有孩子,那么x就是哨兵Nil。但无论x是什么值,x的父节点都是先前y的父节点*/
void RB_Delete_Fixup(Tree* T,Node* x)
{
Node* w;
while(x!=T->Root&&x->color==Black)
{
if(x==x->parent->left)
{
w=x->parent->right;/*进入情况1*/
if(w->color==Red)
{
w->color=Black;
x->parent->color=Red;/*交换颜色后进行左旋操作,新的w为x的新的兄弟节点*/
Left_Rotate(T,x->parent);
w=x->parent->right;
}
if(w->left->color==Black&&w->right->color==Black)/*情况2*/
{
w->color=Red;
x=x->parent;
}
else
{
if(w->right->color==Black)/*右孩子是黑色的,进入情况3*/
{
w->left->color=Black;
w->color=Red;
Right_Rotate(T,w);
w=x->parent->right;
}
else
{
w->color=x->parent->color;/*进入情况4*/
x->parent->color=Black;
w->right->color=Black;
Left_Rotate(T,x->parent);
x=T->Root;
}
}
}
else/*剩下的部分是完全对称的*/
{
w=x->parent->left;
if(w->color==Red)
{
w->color=Black;
x->parent->color=Red;
Right_Rotate(T,x->parent);
w=x->parent->left;
}
if(w->right->color==Black&&w->left->color==Black)
{
w->color=Red;
x=x->parent;
}
else
{
if(w->left->color==Black)
{
w->right->color=Black;
w->color=Red;
Left_Rotate(T,w);
w=x->parent->left;
}
else
{
w->color=x->parent->color;
x->parent->color=Black;
w->right->color=Black;
Right_Rotate(T,x->parent);
x=T->Root;
}
}
}
}
x->color=Black;
}
/*删除红黑树节点的操作,一开始也是和普通二叉树的删除操作基本一样,但也有三点不同
首先,在二叉树中所有的NULL都换做了对Nil节点的引用
其次,不在需要判断x是否为空,直接将x的父节点连接在y上就行了,因为x就算是Nil也是有完整结构的
最后,判断如果删除的y是黑色的,那么调用修正方法*/
Node* RB_Delete(Tree* T,Node* z)
{
Node* y;
Node* x;
if(z->left==&Nil||z->right==&Nil)
y=z;
else
y=Tree_Successor(z);
if(y->left!=&Nil)
x=y->left;
else
x=y->right;
x->parent=y->parent;
if(y->parent==&Nil)
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;
}
if(y->color==Black)
RB_Delete_Fixup(T,x);
return y;
}
/*顺序统计数中用来取出第i小的元素
其实就是x的顺序就保存在他的左孩子的节点上,所以每次和左孩子记录的值进行比较
匹配成功,则直接返回
比左孩子小了,那么在做孩子处递归调用算法
否则在右孩子处递归调用算法*/
Node* OS_Select(Node* x,int i)
{
int r=x->left->Size+1;
if(r==i)
return x;
else if(i<r)
return OS_Select(x->left,i);
else
return OS_Select(x->right,i-r);
}
/*下面的算法给出一个指向节点x的指针,返回他的顺序大小r
其实也是不断的上移,节点x的左孩子的size+1表示他在以他为根节点的子树中的大小
如果x位于右子树,那么加上他的父节点的左子树再加一,直到到达根加点*/
int OS_Rank(Tree* T,Node* x)
{
int r=x->left->Size+1;
Node* y=x;
while(y!=T->Root)
{
if(y==y->parent->right)
r=r+y->parent->left->Size+1;
y=y->parent;
}
return r;
}
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;
N1.Size=N2.Size=N3.Size=N4.Size=N5.Size=N6.Size=N7.Size=N8.Size=1;
//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,根节点size为%d\n",N1.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N2);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N2.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N3);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N3.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N4);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N4.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N5);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N5.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N6);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N6.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N7);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N7.data,T.Root->data,T.Root->Size);
RB_Insert(&T,&N8);
printf("插入节点%d后,根节点为%d,根节点size为%d\n",N8.data,T.Root->data,T.Root->Size);
Mid_Traverse(T.Root);
printf("\n");
Node* S=NULL;
S=OS_Select(T.Root,6);
printf("树中第6小的元素为%d\n",S->data);
printf("树中%d元素处于%d的位置\n",S->data,OS_Rank(&T,S));
//Tree_Delete(T.Root,S);
//Mid_Traverse(T.Root);
return 0;
}