伸展树(Splay Tree)是一种排序二叉树,其核心操作是伸展。所谓伸展就是把指定节点旋转至树根(同时保持排序二叉树性质)的过程。而伸展操作的基础就是旋转。
旋转是所有排序二叉树的基本操作,各种平衡二叉树想要维持其平衡性质都离不开旋转。旋转分为左旋和右旋。但实际上,如果指定节点为左儿子,那么它只能右旋;如果指定节点为右儿子,那么它只能左旋。所以如何旋转可以看作是节点本身的一种性质,而非由外界传参决定。
无论是左旋还是右旋,均是重新确定三对父子关系。上图右旋中,旋转前后有3对父子关系发生了改变,改变后分别是Gt、tP和PB;左旋也一样:Gt、tP和PB。当然祖父节点G不一定存在(P一定存在,因为只会对非根节点做旋转)。
同时,左旋和右旋从某种意义上是对称的。所以可以只用一个函数完成,就称之为旋转。
假设使用静态数组实现二叉树,同时将伸展树的节点定义成如下结构体:
struct node_t{
int parent;
int child[2];
int sn; //本节点是左儿子还是右儿子
//0表示左,1表示右
//...... //其他域省略;
}Node[SIZE]; //Node[0]不使用,用于模拟NULL指针
首先将确定父子关系的代码封装成一个函数,该函数的意思是将p节点的sn儿子设置为t
void _link(int p,int sn,int t){
Node[p].child[sn] = t;
Node[t].parent = p;
Node[t].sn = sn;
}
则旋转操作可以写成:
void _rotate(int t){
int p = Node[t].parent;
int sn = Node[t].sn;
int osn = sn ^ 1;
//确定3对父子关系
_link(p,sn,Node[t].child[osn]);
_link(Node[p].parent,Node[p].sn,t);
_link(t,osn,p);
}
对节点t每完成一次旋转操作,t就会提升一层。不停的旋转,t自然就会达到树根。所以最简单的伸展操作可以这样写(该函数的涵义是在根为root的二叉树中将节点t提升至树根):
void _splay(int t,int& root){
while( Node[t].parent ) _rotate(t);
root = t;
}
但是,还有一种稍微复杂一点但效率更高的用于伸展的旋转方法,称为双旋操作或者之字形旋转或者zig-zag操作等等。双旋操作本质上就是两个旋转操作,所以根本不必费心去画图观察如何实现,只要明确调用旋转的条件即可。
双旋操作是指:如果t及其父亲p的排行(同为左儿子或者同为右儿子)相同,则先旋转p再旋转t;否则,连续旋转t两次。一个双旋可以将t提升两个层次,所以t必须有祖父节点才能进行双旋。当然,此处不必显示的封装一个双旋函数,只需在伸展里面写出即可。使用双旋的伸展函数如下:
void _splay(int t, int& root){
while(Node[t].parent){
int p = Node[p].parent;
if ( p == root ){
_rotate(t);
break;
}
if ( Node[p].sn == Node[t].sn ){
_rotate(p);
_rotate(t);
}else{
_rotete(t);
_rotate(t);
}
}
root = t;
}
注意rotate(t)有多处重复,所以上述代码可以精简一下,变成:
void splay(int t, int& root){
while(Node[t].parent){
int p = Node[t].parent;
if ( p != root ) Node[p].sn == Node[t].sn ? _rotate(p) : _rotate(t);
_rotate(t);
}
root = t;
}
有时候,我们需要将指定节点t伸展成指定节点p的儿子,于是将伸展操作修改如下。当参数p取0时,就是将t伸展成树根。
void splay(int t,int p,int& root){
while( Node[t].parent != p ){
int pp = Node[t].parent;
if ( Node[pp].parent != p ) Node[pp].sn == Node[t].sn ? _rotate(pp) : _rotate(t);
_rotate(t);
}
if ( 0 == p ) root = t;
}