对于被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 ",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