什么是splay?
一种平衡二叉树。
什么是平衡二叉树?
需要先了解什么是:
二叉搜索树——简称BST,每个节点最多有两个子节点,左子比当前节点小,右子比当前节点大。
因此对于插入和查找第k小的值,都可以从根递归着进行下去,在到达递归终点之前,不是选择这个节点左儿子就是右儿子,因此,操作的复杂度 = 树的深度。
然而,这棵树的形状会因为你插入数字的顺序和大小不同,导致层数过大。比如你插入 1 2 3 4 5 6 7按照前面所说,形成的树就是七层了(也就是最坏情况--退化成链状),而你修改一下这七个数插入的顺序,形成的树的形状都和当前这个不同。
这棵树的形状也太随缘了吧,这可怎么办?
平衡二叉树(以下简称平衡树)就是利用各种手段,在不改变中序遍历的情况下搞这个BST的形状,使得BST趋向于平衡,也就是层数变少。
中序遍历:不同于前序遍历先遍历左儿子右儿子再是自己。中序遍历是先左儿子再自己再右儿子。
通过中序遍历,我们可以按照从小到大的顺序获得这棵BST上的值。
各种平衡树,如treap,AVL,红黑树,SBT,splay等,都是在不改变BST中序遍历结果的情况下改变树的结构。
而中序遍历不变,那么这两颗BST是等价的:因为新的树仍然拥有原树的所有数据,并且中序遍历结果不变,意味着仍旧符合BST的性质。
换种好理解的方式来说明的话:我们说1234567这种方式的插入是形成的BST最差的,各种平衡树的平衡操作,本质上就是不改变插入的是1234567这七个数,而是通过对BST的摆弄,相当于做了改变插入顺序形成形状更好的树(注意我只是说相当于),比如说4213657这个顺序插入形成的树就非常nice。
Splay如何平衡?
Splay平衡的操作就叫做splay(伸展),这个操作基于一种叫rotate(旋转)的操作。
rotate(其实是很多平衡树的基础操作)
旋转分为左旋和右旋。
左旋就是把左儿子转到父亲的位置,让父亲变为自己的右儿子,并让左儿子的右儿子成为父亲的左儿子。
右旋就是把右儿子转到父亲的位置,让父亲变为自己的左儿子,并让右儿子的左儿子成为父亲的右儿子。
如图
代码
const int N = 1e5+5;
int fa[N];//父亲是哪个节点
int ch[N][2];//数组第二维0,1分别表示左右儿子
void rotate(int x){ //x为将要旋转到父亲的节点,此函数能调用的条件是x有父亲
int f = fa[x]; //x的父亲(虽然快要给x当儿子了)
int d = ch[f][0] == x? 1:0; //判断是需要左旋还是右旋,d = 1表示右旋,d = 0表示左旋
fa[x] = fa[f],fa[f] = x,fa[ch[x][d]] = f; //正确维护旋转后的fa数组
ch[x][d] = f,ch[f][d^1] = ch[x][d]; //改变父子关系,d^1等价于:1变0,0变1
if (fa[x]) ch[fa[x]][ch[fa[x]]==f?0:1] = x;//原本f有父亲的话,现在需要连向x,第二维里面的那个式子是在判断f原先是其父亲的左儿子还是右儿子
push_up(f),push_up(x); //像线段树一样push_up以正确维护当前节点的信息,注意顺序!!!要先f再x
}
splay
splay其实就是不停的rotate。
splay需要传入两个参数x,goal,第一个就是需要splay的节点,第二个就是x需要不停向上转向上转,直到转到以goal为父节点。
(x:我往上转往上转,我一定要做goal的儿子啊啊啊啊啊)
当然具体没那么简单。
定义f为x的父亲、g为f的父亲,也就是x的爷爷。
我们现在要把x转到g,需要转两次,这里怎么转就有讲究了。
不会证明但是可以把BST搞平衡的旋转方法:
如果x,f,g三点一线,那么先旋转f,再旋转x
否则旋转两次x
反正...这么旋转就能平衡,每次插入一个数之后马上splay这个点到根节点这棵树就平衡了。啥均摊logN的咱也不懂,感兴趣的可以去看看证明。
代码
void splay(int x,int goal=0){//第二个蚕食不传入默认为0,只有fa[root] = 0,因此默认splay到根节点
while (fa[x] != goal){
int f = fa[x],g = fa[f];
if (g!=goal) rotate((ch[f][0]==x)==(ch[g][0]==f)? f:x);//g==goal表示转一次父亲就是goal了,这个if就不用进了
rotate(x); //然后上面的if里面就是在判断祖孙仨儿是不是三点一线
}
if (!goal) root = x;//此时修改根节点
}
以上就是基本平衡的操作,具体怎么实现插入删除查找通过下面的题来学!
>>>洛谷3369
6种操作:
①插入一个数
②删除一个数
③查x的排名
④查排名为x
⑤求x的前驱(比x小的最大的数)
⑥求x的后继(比x大的最小的数)
咱也分六部分讲吧,先给出开了那些数组和基本的函数,最难是删除,咱放最后讲。
int ch[N][2];//两个儿子
int fa[N];//父亲
int size[N];//子树大小(节点数)
int cnt[N];//该点的值出现了几次(插入了几个),题目明确说没有插入重复数字就可以不用这个数组,不过其实就算有也可以不用
int val[N];//节点的值
int sz,root;//sz是下一个新建节点的下标,root是根
void init(){
sz = 1;
root = 0;
}
int newnode(int v,int f){//v,f是将要新建节点的值,父亲
ch[sz][0] = ch[sz][1] = 0;
fa[sz] = f;
size[sz] = cnt[sz] = 1;
val[sz] = v;
return sz++;//返回新节点下标,并自增1
}
void push_up(int rt){
size[rt] = cnt[rt] + size[ch[rt][0]] + size[ch[rt][1]];
}
void rotate(int x){
int f = fa[x];
int d = ch[f][0]==x? 1:0;
fa[x] = fa[f],fa[f] = x,fa[ch[x][d]] = f;
ch[f][d^1] = ch[x][d];
ch[x][d] = f;
if (fa[x]) ch[fa[x]][ch[fa[x]][0]==f?0:1] = x;
push_up(f);push_up(x);
}
void splay(int x,int goal=0){
while (fa[x] != goal){
int f = fa[x],g = fa[f];
if (g!=goal) rotate((ch[f][0]==x)==(ch[g][0]==f)? f:x);
rotate(x);
}
if (!goal) root = x;
}
插入
其实在讲splay的时候我们讲过了。
就找到合适的位置然后让父亲链上他,然后splay这个点到根就可以让树平衡了。
void insert(int v){
int now = root,f=0;//f记录新建节点的父亲,一定要初始化为0
while (now && val[now]!=v) f = now,now = ch[now][v>val[now]];
if (now) cnt[now]++;//这个节点已经有了就不用新建,直接次数+1
else {now = newnode(v,f);if (f)ch[f][v>val[f]] = now;}//newnode用于新建一个节点,然后一定要连到树上啊
splay(now);//通过splay重构了一下树,并(主要是)正确更新了原本由于新插入一个节点而产生错误的祖先节点的信息
}
查x的排名
也就是一直深搜下去把左子树的size一直累积直到当前节点的值就是v,注意看那行注释
int rank(int v){
int now = root,ans = 0;
while (now){
if (val[now]>v) now = ch[now][0];
else{
ans += size[ch[now][0]];
if (val[now]==v) {splay(now);return ans+1;}//此处需要splay的原因是为了方便求前驱后继,否则可以不要