【学时总结】◆学时·VI◆ SPLAY伸展树

时间:2021-08-13 15:50:22

◆学时·VI◆ SPLAY伸展树

平衡树之多,学之不尽也……


◇算法概述

二叉排序树的一种,自动平衡,由 Tarjan 提出并实现。得名于特有的 Splay 操作。

Splay操作:将节点u通过单旋、双旋移动到某一个指定位置。

主要目的是将访问频率高的节点在不改变原顺序的前提下移动到尽量靠近根节点的位置,以此来解决同一个(相似)问题的多次查询。

但是在非降序查询每一个节点后,Splay 树会变为一条链,降低运算效率。


◇原理&细解

(1)旋转操作

二叉排序树必须满足 左儿子<根节点<右儿子 ,即使在旋转过后也是如此。因此旋转操作(Rotate)是Splay平衡树的一个重要组成部分。而在Splay操作中,旋转分单旋和双旋。

单旋:

【学时总结】◆学时·VI◆ SPLAY伸展树

由于Rotate分成两种情况,许多OIer直接把两种情况分类讨论写在程序里,这样就使得Rotate()函数及其之长。但是老师教了我们一个不错的俭省代码的方法(~^o^~):

首先我们定义x的左儿子为 tree[x].ch[0],右儿子为 ch[1],再在Rotate()函数的参数表中加上"d",d=1表示右旋,0表示左旋。

void Rotate(Node *x,int d)
{
Node *y=x->fa;
y->ch[!d]=x->ch[d];x->fa=y->fa;
if(x->ch[d]!=NULL) x->ch[d]->fa=y;
if(y->fa!=NULL) y->fa->ch[y->fa->ch[]==y]=x;
y->fa=x;x->ch[d]=y;
if(y==root) root=x;
Update(y);
}

完美地契合了上图的规律,从而达到简短代码的目的!

双旋:

不用怎么解释……其实就是3个点(儿子X,父亲Y,祖父Z)之间将儿子X转移到祖父Z位置的2次旋转操作。第一次旋转能够将儿子X旋转到父亲Y位置,此时的旋转和祖父Z没有关系,就看成X,Y的旋转;第一次旋转后,Y就成了X的一棵子树,所以第二次旋转是Z和X之间的……总而言之就是两次单旋,只是注意旋转方向,保证原有关系不变。

举个例子:

【学时总结】◆学时·VI◆ SPLAY伸展树

reader 们可以把剩下的3种自己试一试,有什么不懂的可以在文末的邮箱处ask我 (^.^)~

(2)SPLAY操作

实质是旋转的组合……

作为Splay树的核心,它能够实现将指定节点旋转到某一个位置(或某一个节点的儿子的位置)的操作。通过Splay操作,我们可以每一次将查询的节点向高处提,从而下一次访问该节点时速度加快。

设当前需要转移的节点为x,节点y,z分别是它的父亲,祖父,x需要转移到节点rt的下方。由于每一次Rotate操作每一次可以使节点上移一层(目的一般不会是下移),如果z就是rt,就说明y是x要到达的地方(因为z的下面就是y),而x到y只需要一次Rotate,因此调用单旋。

其他情况下至少需要两次Rotate操作,即双旋。直到到达目标位置为止。

如何判断是左旋还是右旋?

我们很容易发现一个规律——如果要使V上移到U(U是V的父亲),当V是U的左儿子时,我们需要右旋,而V是U的右儿子时,需要左旋……也就是说儿子的左右和旋转方向的左右是恰好相反的。

(3)查找树中是否存在某个节点

这是所有操作中最简单的一个,只用到了二叉排序树的性质。

设查找点的值为val,从根节点开始查找,设当前查找到的点值为u。由于根的左子树小于根,而右子树大于根,所以u>val时向左子树查询,否则向右子树查询,直到查找到值或者当前节点为空NULL。

(4)插入一个特定值的节点

基本思想和查找节点很像,也是根据二叉排序树来确定位置。

当我们找到一个值恰好为特定值的节点,则将该节点的个数+1,不再插入节点了。与查找不同的是它如果按顺序查找节点,发现该节点为NULL,就说明没有值为val的节点,此时我们会新建一个值为val的节点插入到那个为NULL的节点。

(5)查询点排名以及特定排名的点

这里的排名不包括并列(2,3,3,4 3的排名为2或3,4的排名为4)。其实就是比他小(严格小于)的元素个数+1,而比他小的元素恰好就是他的左子树,因此也就是它的左子树的个数+1。

查找特定排名的点要麻烦一些……设当前节点为u,当u的左子树+1大于排名,则说明当前数过大,向左子树查询,否则向右子树查询。若查询右子树,则先将特定排名减去当前节点的左子树大小,表示在右子树中需要找到第"特定排名减去当前节点的左子树大小"大的元素。

换句话说,当前节点为u,向u的右子树查询,则目标节点在u的右子树中的排名为 (以u为根的子树中的排名 - u的左子树大小)。

(6)删除特定值的点

还是先像查找特定值的节点的思路,先找到要删除的节点的位置。由于我把值相同的点压缩在了一个点上,值相同的点的个数为cnt。当cnt>1时,即不止一个点值为特定值,我们可以直接cnt--;如果cnt=1,则删除该点后,该点就没了……这时候我们需要处理节点与其前驱后继的关系。我们可以把前驱通过Splay移动到根节点,而把后继移到前驱的右儿子。我们会发现后继的左儿子就是要删除的节点,且它没有儿子(叶结点),所以我们直接把左儿子改为NULL,再Update更新节点个数,好像就完了(=^-ω-^=)


◇手打简单模板

(PS.下面这个模板实现了插入Insert,删除Delete(无法判断是否存在该元素),查找节点GetKey,正反向查询排名Find_Count/Get_Num,查找前驱后继FrontBehind)

 /*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct Node{
Node *ch[],*fa; //ch[0]左儿子,ch[1]右儿子
int v,cnt,size; //v点权,cnt点权为v的点数量,子树大小(包括根节点)
Node(int v):v(v){ //初始化
fa=ch[]=ch[]=NULL;
cnt=;
}
int cmp(int x)const { //某时候极其方便的比较函数
if(x==v) return -;
return x<v? :;
}
}*root; //树根
int Get_Size(Node *p) //避免点为NULL时访问size错误
{
return p==NULL? :p->size;
}
void Update(Node *x) //上传子树大小
{
x->size=+Get_Size(x->ch[])+Get_Size(x->ch[]);
}
void Rotate(Node *x,int d) //旋转,d=0左旋,d=1右旋
{
Node *y=x->fa;
y->ch[!d]=x->ch[d];x->fa=y->fa;
if(x->ch[d]!=NULL) x->ch[d]->fa=y;
if(y->fa!=NULL) y->fa->ch[y->fa->ch[]==y]=x;
y->fa=x;x->ch[d]=y;
if(y==root) root=x;
Update(y);
}
void Splay(Node *x,Node *rt)
{
while(x->fa!=rt) //直到到达目标位置为止
{
Node *y=x->fa;Node *z=y->fa;
if(z==rt) //只旋转一次即到目标位置
if(x==y->ch[]) Rotate(x,);
else Rotate(x,);
else //双旋
if(y==z->ch[])
if(x==y->ch[])
Rotate(y,),Rotate(x,);
else
Rotate(x,),Rotate(x,);
else
if(x==y->ch[])
Rotate(y,),Rotate(x,);
else
Rotate(x,),Rotate(x,);
}
Update(x);
}
void Insert(int val) //插入值为val的节点
{
if(root==NULL) {root=new Node(val);return;}
//插入节点
Node *y=root;
while(true)
{
if(val==y->v) {y->cnt++;Splay(y,NULL);return;}
//如果已经存在值为val的节点,则该节点个数+1
Node *&ch=(val<y->v? y->ch[]:y->ch[]);
if(ch==NULL) break;
y=ch;
}
Node *x=new Node(val);
(val<y->v? y->ch[]:y->ch[])=x;
x->fa=y;
Splay(x,NULL);
}
Node *Find(Node *x,int d) //寻找前驱后继(d=0前驱,d=1后继),只能寻找已存在于树中的值
{
while(x && x->ch[d]) x=x->ch[d];
return x;
}
void Delete(int num) //删除一个值为num的节点
{
Node *p=root;
while(true)
{
if(!p) return;
if(p->v==num)
{
Splay(p,NULL);
if(p->cnt==) //单个节点
{
Node *Front=Find(p->ch[],),
*Behind=Find(p->ch[],); //处理前驱后继
if(!Front && !Behind) root=NULL;
else if(!Front) root=root->ch[],root->fa=NULL;
else if(!Behind) root=root->ch[],root->fa=NULL;
else
{
Splay(Front,NULL);
Splay(Behind,root);
root->ch[]->ch[]=NULL;
root->ch[]->size--;
}
}
else p->cnt--; //减少个数
return;
}
p=p->v>num? p->ch[]:p->ch[];
}
}
Node *GetKey(Node *o,int x) //根据二叉排序树关系查找值为x的节点
{
int d=o->cmp(x);
if(d==-) return o;
return GetKey(o->ch[d],x);
}
int Find_count(int val) //找到值为val的节点在树上的排名
{
Node *x=GetKey(root,val);
Splay(x,NULL);
return Get_Size(x->ch[])+;
}
int Get_Num(int num) //找到排名为num的数
{
Node *now=root;
while(now)
{
if(num>=Get_Size(now->ch[])+ && num<=Get_Size(now->ch[])+now->cnt)
break;
if(Get_Size(now->ch[])>=num) now=now->ch[];
else
{
num-=Get_Size(now->ch[])+now->cnt;
now=now->ch[];
}
}
Splay(now,NULL);
return now->v;
}
int FrontBehind(int num,int d) //找前驱后继(不一定在树上)
{
Insert(num);
int res=Find(root->ch[d^],d)->v;
Delete(num);
return res;
}
int main()
{
while(true)
{
int cmd,x;
scanf("%d%d",&cmd,&x);
switch(cmd)
{
case : Insert(x);break;
case : Delete(x);break;
case : printf("%d\n",Find_count(x));break;
case : printf("%d\n",Get_Num(x));break;
case : printf("%d\n",FrontBehind(x,));break;
case : printf("%d\n",FrontBehind(x,));break;
}
}
return ;
}

这个代码风格可能比较奇怪,因为是从几个不同的代码裁剪修改,然后组合起来的……(∩╹□╹∩)


The End

Thanks for reading!

- Lucky_Glass

(Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)