对于被AVL虐得像那啥一样的我们,Splay的到来是无疑是拯(huo)救(shang)人(jiao)民(you)。
Splay树,又称伸展树,事实上,它根本就不是平衡树!然而它的平均时间复杂度确是O(log n)。
唯一和AVL树一样的是:转转转,转转转……
#----------------------------------------------------------------------------------------------#
如果你不知道什么是图,你可以再见了
如果你不知道什么是树,你可以再见了
如果你不知道什么是二叉树,你可以再见了
#----------------------------------------------------------------------------------------------#
如果你不知道什么是二叉排序树,那,就讲讲吧:
专业解释(二叉查找树就是二叉排序树):
设该结点为x,值为num:
①x的左子树中所有结点的值小于num
②x的右子树中所有结点的值大于num
那么就有趣了:这个二叉树的中序遍历是一个上升(不下降)序列。
当然也可以左子树大于num,右子树小于num,那么中序遍历就是一个下降(不上升)序列。
下图即是二叉查找树
至于怎么写,网上两堆(大于一堆)
#----------------------------------------------------------------------------------------------#
如果你不知道平衡树:
因为我们发现:普通二叉查找树有很多优点,比如插入删除查找较快
但是有的时候,比如树退化成了链(对于万恶的出数据者来说,这是很容易出现的)时,以上操作就很耗时(很耗时?很好使?)了,几乎为O(N)。
那么很么时候能快一些呢,容易发现:在当前结点的左右子树相差尽量小时,以上操作就快了。
简单说:平衡树就是高度为O(log n)的树。
也可以这样说:
设当前结点左子树高度为h1,右子树高度为h2,则|h1-h2| ≤ 1,这个|h1-h2|称为该结点的平衡因子
如果每次都能保持这样,就快了(O(log n))
ps.二叉搜索树也是二叉查找树
#----------------------------------------------------------------------------------------------#至于实现,请看下面(以下所有平衡树都指左儿子比自己小,右儿子比自己大的二叉查找树):
#----------------------------------------------------------------------------------------------#
如果你不知道AVL:
事实上,AVL平衡树就是:每插入一个结点便进行一次边的“旋转”,使高度平衡。
旋转,分左旋(zag),右旋(zig),左旋右旋(zagzig),右旋左旋(zigzag)
使用的条件是什么呢?在图上不难看出:
简单点说:
"/"用zig,"\"用zag,"<"用zigzag,">"用zagzig
至于实现,不用在网上找了,基本上没有,我也不会给你的。
int zig(int r)//右旋 // 参数为要旋转的“根”
{
int t=tree[r].lc;
tree[r].lc=tree[t].rc;//左儿子接在左儿子的右儿子上
tree[t].rc=r;//当前结点接到它左儿子的右儿子上
tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;//更新高度(深度)
tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
return t;//返回现在的“根”
}
int zag(int r)//左旋
{
int t=tree[r].rc;
tree[r].rc=tree[t].lc;
tree[t].lc=r;
tree[r].h=max(tree[tree[r].rc].h,tree[tree[r].lc].h)+1;
tree[t].h=max(tree[tree[t].rc].h,tree[tree[t].lc].h)+1;
return t;//与右旋完全相反
}
int zagzig(int r)//右旋左旋
{
tree[r].lc=zag(tree[r].lc);//先左旋左儿子
return zig(r);//在右旋自己
}
int zigzag(int r)//左旋右旋
{
tree[r].rc=zig(tree[r].rc);
return zag(r);//与右旋左旋相反
}
可以自己画图验证。
所以,插入实现:
int insert(int x,int root)
{
if(!root)//到了叶子
{
tree[++cnt].num=x;
tree[cnt].h=1;//新加入一个结点
return cnt;
}
if(x<tree[root].num)//小于当前值=>找左儿子
{
tree[root].l=insert(x,tree[root].l);//递归找左儿子
if(tree[tree[root].l].h==tree[tree[root].r].h+2)//不平衡了
{
if(tree[tree[root].l].num>x) root=zig(root);//出现了"/"型
else if(tree[tree[root].l].num<x) root=zagzig(root);//出现了"<"型
}
}
else if(x>tree[root].num)//大于当前值=>找右儿子
{
tree[root].r=insert(x,tree[root].r);
if(tree[tree[root].r].h==tree[tree[root].l].h+2)
{
if(tree[tree[root].r].num<x) root=zag(root);
else if(tree[tree[root].r].num>x) root=zigzag(root);
}//与上面类似
}
tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;//更新高度
return root;//返回现在的根
}
那么,删除怎么写呢?这就有毛病了:极其麻烦……
void maintain(int &root)//先看下面的dale
{
if(tree[tree[root].l].h==tree[tree[root].r].h+2)
{
int t=tree[root].l;
if(tree[tree[t].l].h==tree[tree[root].r].h+1)
root=zig(root);
else if(tree[tree[t].r].h==tree[tree[root].r].h+1)
{
tree[root].l=zag(tree[root].l);
root=zig(root);
}
}
else if(tree[tree[root].l].h+2==tree[tree[root].r].h)
{
int t=tree[root].r;
if(tree[tree[t].r].h==tree[tree[root].l].h+1)
root=zag(root);
else if(tree[tree[t].l].h==tree[tree[root].l].h+1)
{
tree[root].r=zig(tree[root].r);
root=zag(root);
}
}
tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;//与插入的调整类似
}
int dale(int &root,int x)//删除值为x的结点
{
int t;
if(x==tree[root].num||(x<tree[root].num&&tree[root].l==0)||(x>tree[root].num&&tree[root].r==0))//找到这个结点或者找到了它“可能”在的地方:比当前节点大就找右儿子,反之则找左儿子
{
if(tree[root].l==0||tree[root].r==0)//左右儿子中至少一个为空
{
t=tree[root].num;
root=tree[root].l+tree[root].r;//删除
return t;
}
else
tree[root].num=dale(tree[root].l,x);//否则继续找
}
else
{
if(x<tree[root].num) t=dale(tree[root].l,x);
else t=dale(tree[root].r,x);
}
maintain(root);//调整根(保持平衡)
return t;
}//注意,这个删除不管有没有值为x的结点,都会删掉一个结点,如果没有值为x的结点,它会删掉x的前驱
AVL就是这些了,例题:宠物收养所。
#----------------------------------------------------------------------------------------------#
接下来是Splay:
之前也说了,Splay也是转转转。
但是,对于AVL,调整最多只是一次双旋,而Splay,是直接把此节点转到根(或其他位置)上(称为“伸展”)!
zig和zag是没有区别的,zigzag,zagzig有一点区别:AVL为先转下面(儿子),再转上面(自己),而Splay的双旋是先转上面,再转下面,为什么?time!time!time!要快一些。
显然,要把结点转到根上是要转很多次的,你当然可以选择每次都zig或者zag,但是time!time!time!能双旋就尽量双旋,原因已说3遍。
所以,Splay根本就不是平衡树,可以说是“伪”平衡树,但多次操作后时间复杂度是极其接近O(log n)的。
而且,很多操作,对于Splay是很容易,对于AVL却是极难的。
如:
①合并两棵树A,B(A中所有结点都小于B中所有结点):把A中最大的结点伸展到根,很显然它是没有右儿子的,然后直接使B的根为它的右儿子即可。
②以结点x为界,分为两棵树:将x伸展到根,剪断左右儿子即可。
③对区间[x,y]进行操作:找到x的前驱xl,y的后继yn,将xl伸展到根,yn伸展到根(xl)的右儿子处(先不着急怎么实现),这样根的右儿子的左子树就是区间[x,y]了,很容易理解。
接下来就是实现了,其实如果写的漂亮,50行(和AVL的150行比起来根本不算什么)以内的基本代码是可以的(造福人民啊啊),但是,如果你按我刚刚说的一点一点码,我觉得:你还是写AVL吧……不要有歧义,我的意思不是copy……
此题为“营业额统计”,题目在网上可以找到:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define MAXN 100005
#define INF 10000000
int ch[MAXN][2],fa[MAXN],sale[MAXN];//ch为左右儿子(存在0列和1列),fa是父亲,sale是结点的值
int ans,tot,root;//root存根的位置
#define min(a,b) ((a)>(b)?(b):(a))
void rotato(int x)//这个旋转包括了zig,zag,zigzag,zagzig的所有情况
{
if(x==0||fa[x]==0)return;
int y=fa[x],z=fa[y];
bool flag=(ch[y][1]==x);
ch[y][flag]=ch[x][!flag];
if(ch[y][flag])fa[ch[y][flag]]=y;
ch[x][!flag]=y;
fa[y]=x;
fa[x]=z;
if(z)ch[z][ch[z][1]==y]=x;//同样自己用一组数据试一试就知道了
}
void splay(int x,int goal)//goal为旋转到的位置的父亲的位置(解决了刚刚旋转位置不一定是跟的情况,根的话goal就为0)
{
for(int y;(y=fa[x])!=goal;rotato(x))//由于用了很多类似“a[x==y]”“(x=y)==z”的语句,可能会比标准写法或者AVL树慢几毫秒
{
int z;
if((z=fa[y])!=goal)
{
if((ch[z][1]==y)==(ch[y][1]==x))//看是左儿子还是右儿子
rotato(y);
else rotato(x);
}
}
if(goal==0)root=x;//如果是旋转到根,那root就要改一下
}
int insert(int r,int x)//在插入时顺便找到相差最小的值
{
int tmp=INF,f=0;
while(r&&sale[r]!=x)
{
f=r;
if(x<sale[r])
{tmp=min(sale[r]-x,tmp);
r=ch[r][0];
}
else
{
tmp=min(x-sale[r],tmp);
r=ch[r][1];
}
}//递归会极其(是的,直接TLE)耗时
if(r==0)//如果找到根(没有x(因为有可能树中已经有x了,就不用再插入))
{
sale[++tot]=x;
r=tot;
fa[r]=f;
if(sale[f]>x)ch[f][0]=tot;
else ch[f][1]=tot;
splay(r,0);//伸展
return tmp;
}
else
{
splay(r,0);
return 0;
}
}
int main()
{
int n,t;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&t);
if(i) ans+=insert(root,t);
else
{
ans=t;
insert(root,t);
}
}
printf("%d\n",ans);
}
这是AVL代码:
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAXN 100000
struct node
{
int num;
int l,r,h;
}tree[MAXN+5];
int N,R;
int cnt;
bool F;
void read(int &x)
{
int f=1; x=0; char s=getchar();
while(s<'0'||s>'9'){if(s=='-') f=-1;s=getchar();}
while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
x*=f;
}
int zig(int x)
{
int t=tree[x].l;
tree[x].l=tree[t].r;
tree[t].r=x;
tree[x].h=max(tree[tree[x].r].h,tree[tree[x].l].h)+1;
tree[t].h=max(tree[tree[t].r].h,tree[tree[t].l].h)+1;
return t;
}
int zag(int x)
{
int t=tree[x].r;
tree[x].r=tree[t].l;
tree[t].l=x;
tree[x].h=max(tree[tree[x].r].h,tree[tree[x].l].h)+1;
tree[t].h=max(tree[tree[t].r].h,tree[tree[t].l].h)+1;
return t;
}
int zigzag(int x)
{
tree[x].r=zig(tree[x].r);
return zag(x);
}
int zagzig(int x)
{
tree[x].l=zag(tree[x].l);
return zig(x);
}
int insert(int x,int root)
{
if(!root)
{
tree[++cnt].num=x;
tree[cnt].h=1;
return cnt;
}
if(x<tree[root].num)
{
tree[root].l=insert(x,tree[root].l);
if(tree[tree[root].l].h==tree[tree[root].r].h+2)
{
if(tree[tree[root].l].num>x) root=zig(root);
else if(tree[tree[root].l].num<x) root=zagzig(root);
}
}
else if(x>tree[root].num)
{
tree[root].r=insert(x,tree[root].r);
if(tree[tree[root].r].h==tree[tree[root].l].h+2)
{
if(tree[tree[root].r].num<x) root=zag(root);
else if(tree[tree[root].r].num>x) root=zigzag(root);
}
}
tree[root].h=max(tree[tree[root].l].h,tree[tree[root].r].h)+1;
return root;
}
int minx(int x,int root)
{
if(root==0) return 0x7fffffff;
if(tree[root].num==x)
{
F=1;
return 0;
}
if(tree[root].num<x)
return min(x-tree[root].num,minx(x,tree[root].r));
else
return min(tree[root].num-x,minx(x,tree[root].l));
}
int main()
{
int sum=0;
read(N);
for(int i=1;i<=N;i++)
{
int x;
read(x);
if(i>1)
sum+=minx(x,R);
else
sum+=abs(x);
if(!F)
R=insert(x,R);
F=0;
}
printf("%d",sum);
}
没有对比就没有伤害~尽管要慢一些(而且AVL还用了读入优化),但这个整整短了4,50行啊!
好了,不知道这篇和NOIP复习那篇哪篇长一点……
By WZY