前言
splay是一个维护序列(一堆数)比较好的数据结构,他通过双旋可以有效的避免二叉排序树退化成一条链的情况(证明可以看论文),让平均复杂度在logn,他可以维护一个序列即:有n个数字,每个数字的位置跟值都告诉你了,有许多在某个区间(位置)上的操作。还可以维护一堆数字即:没有位置的概念,就是给你一堆数字,没有在区间上的操作,通常存在序列区间上反转,交换,插入删除新的数字,求第k个位置的数字的时候会用到splay,当然维护一堆数字的也是可以因为二叉搜索树容易退化成on的情况,另外值得一提的是在序列上操作很多都是跟线段树一样的,比如维护极值跟维护和。下面就找了几篇教学博客~
tabris大佬的教程
前言
最近3个月内,无论是现场赛还线上赛中SPLAY出现的概率大的惊人啊啊啊!!!
然而不会的我就GG了,同时发现大家都会SPLAY,,,,然后就学习了一波。
开始怎么学都学不懂,直到看到一句话
想学好splay,只要把伸展和旋转操作弄懂,就好了.
(而这两个想要学会就是需要自己画图自己理解了)
于是茅塞顿开,有了本文,
本文重点是SPLAY维护序列的操作,而非SPLAY本身,这部分会说的比较粗略,二叉树的部分更不会有说明,
菜(sha3)逼我也只是初学,如果有描述不当甚至错误的地方,欢迎指正
定义
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。
同其他平衡树一样,都是在二叉排序树的基础上进行操作的,但不同于AVL需要记录平衡信息,也没有红黑树实现上的难度.是一种综合考量下很适合应用于信息学竞赛的平衡树.
对于一个基本的SPLAY 我这样定义
int ch[N][2]; //ch[][0] 表示左儿子 ch[][1] 表示右儿子
int f[N]; //节点的父亲节点
int sz[N]; //当前节点给所在的子树的节点个数
int val[N]; //当前节点表示的值
int cnt[N]; //当前节点所表示的值的个数
int root; //记录根节点的
int tot; //计算树中节点个数
构建的过程也就和普通二叉树一样了,递归下去即可
void newnode(int rt,int v,int fa){
f[rt]=fa;
val[rt]=v;sz[rt]=1;
ch[rt][0]=ch[rt][1]=0;
}
void delnode(int rt){
f[rt]=sz[rt]=val[rt]=0;
ch[rt][0]=ch[rt][1]=0;
}
void build(int &rt,int l,int r,int fa){
if(l>r) return ;
int m = r+l >> 1;
rt=m; newnode(rt,val[rt],fa);cnt[rt]=1;
build(ch[rt][0],l,m-1,rt);
build(ch[rt][1],m+1,r,rt);
pushup(rt);
}
void init(int n){
root=0;
f[0]=sz[0]=ch[0][0]=ch[0][1]=rev[0]=0;
build(root,1,n,0);
pushup(root);
}
旋转
对于一颗二叉排序树,根据序列的信息很容易找到某一个值,只要不断的向下搜索下去即可,复杂度是O(树高),
但是二叉树最坏的情况下是会退化成一个单链的,这是后查找的复杂度就是O(n)了,非常不可取
而在SPLAY中控制树保持平衡需要的就是旋转操作,是树保持平衡,这样复杂度就变成了均摊O(logn)O(logn)的了
单旋: 左旋(zag)&右旋(zig)
双旋:
通过树的旋转来自我调整来保持平衡,就是基于这两个操作左旋(zag)&右旋(zig),还有其延伸出的操作
下面来实现下旋转操作
总之就是对每次旋转节点间关系信息发生改变的位置调整好就行
需要点耐心,不要调错
void rotate(int x,int k){ // k = 0 左旋, k = 1 右旋
int y=f[x];int z=f[y];
pushdown(y),pushdown(x);
ch[y][!k]=ch[x][k];if(ch[x][k])f[ch[x][k]]=y;
f[x]=z;if(z)ch[z][ch[z][1]==y]=x;
f[y]=x;ch[x][k]=y;
pushup(y),pushup(x);
}
伸展
经过多次旋转,将节点位置坐出调整的操作就是伸展了
来举个栗子,对于一个退化为单链的树进行旋转
双旋的写法,比较稳定
void splay(int x,int goal){
//将x旋转到goal的下面
while(f[x] != goal){
if(f[f[x]] == goal) rotate(x , ch[f[x]][0] == x);
else {
int y=f[x],z=f[y];
int K = (ch[z][0]==y);
if(ch[y][K] == x) rotate(x,!K),rotate(x,K);
else rotate(y,K),rotate(x,K);
}
}
pushup(x);
if(goal==0) root=x;
}
而我发现zig-zag这种两个旋转合在一起的操作,其实是两遍单旋,所以只要每次都向上单旋就行了,
单旋容易被卡 不懂 百度伸展树单双旋的比较
void splay(int x,int goal){ //将x调整为goal的儿子,(如果要调整到根goal就是0)
for(int y=f[x];f[x]!=goal;y=f[x])
rotate(x,(ch[y][0]==x)); //(ch[y][0]==x)计算是左旋还是右旋,看x是左右儿子哪一个区分开了
if(goal==0) root=x;
}
各种操作
对于SPALY能够做到的操作以【BZOJ3224 普通平衡树】为引,以 【BZOJ 1895 & POJ 3580 supermemo 】做补充,如果不完全,后期会补上
一些基本操作
查找
查找部分和普通的二叉查找树一模一样,只要遍历下去即可
int search(int rt,int x){
if(ch[rt][0]&&val[rt]>x) return search(ch[rt][0],x);
else if(ch[rt][1]&&val[rt]<x)return search(ch[rt][1],x);
else return rt;
}
极值 & 前驱,后继
前驱:小于x的最大的数
后继:大于x的最小的数
先找到x所在的节点,然后在左右子树,找最右左的节点即可
//以x为根的子树 的极值点 0 极小 1 极大
int extreme(int x,int k){
while(ch[x][k])x=ch[x][k];splay(x,0);
return x;
}
第K个数
第k个数,通过记录的sz[],很容易得到每个节点是第几个,不断的在树上二分就行,
//以x为根的子树 第k个数的位置
int kth(int x,int k){
if(sz[ch[x][0]]+1==k&&k<=sz[ch[x][0]]+cnt[x]) return x;
else if(sz[ch[x][0]]>=k) return kth(ch[x][0],k);
else return kth(ch[x][