Splay树详解
Posted gzh-red
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Splay树详解相关的知识,希望对你有一定的参考价值。
Splay树
这是一篇宏伟的巨篇
首先介绍BST,也就是所有平衡树的开始,他的China名字是二叉查找树.
BST性质简介
给定一棵二叉树,每一个节点有一个权值,命名为 关键码 ,至于为什么叫这个名字,我也不知道.
BST性质也就是,对于树中任何一个节点,都满足一下性质.
\\1. 这个节点的关键码不小于它的左子树上,任意一个节点的关键码
\\2. 这个节点的关键码不大于它的右子树上,任意一个节点的关键码
然后我们就可以发现这棵树的中序遍历,就是一个关键码单调递增的节点序列,说的直白点,就是一个排好序的数组.
这就是伟大的BST性质,一个引起无数OIer憎恨的性质.(手动滑稽)
Splay的背景
什么是Splay,它就是一种可以旋转的平衡树.它可以解决BST树这棵树一个极端情况,也就是退化情况.
如下图所示.
我们发现上面这个图是一条链,这种恐怖的数据让时间从O(log(n))O(log(n))退化到O(n)O(n).
于是各种各样的科学家们,就开始了思考人生,开始了丧心病狂地创造出了各种平衡树方法,然后一个异常有名的科学家Tarjan开始了它丧心病狂的Tarjan算法系列.
Splay思路
这是一棵特殊的BST树,或者说平衡树基本都是改变树结构样式,但是却不改变最后得出的排列序列.
来一个yyb大佬的美图
这张图片大致意思如下所示:正方形部分表示一棵子树,然后圆形表示节点.(当然了其实你也可以都看作成为节点,可能更好理解吧,看个人看法)
对于这样一棵树,我们可以做一些特殊的操作,来让它变换树的形态结构,但是最后的答案却是正确的.平衡树的精髓就是这个,就是改变树的形态结构,但是不改变最后的中序遍历,也就是答案数组.
重点来了,以下为splay精华所在之处,一定要全神贯注地看,并且手中拿着笔和草稿纸,一步步跟着我一起做,那么我保证你看一遍就可以懂.相信自己一定可以明白的,不要因为文字太多而放弃.
现在我们的现在目标只有一个x节点往上面爬,爬到它原本的父亲节点y,然后让y节点下降
首先思考BST性质,那就是右子树上的点,统统都比父亲节点大对不对,现在我们的x节点是父亲节点y的左节点,也就是比y节点小. 那么我们为了不改变答案顺序,我们可以让y节点成为x节点的右儿子,也就是y节点仍然大于我们的x节点
这么做当然是没有问题的,那么我们现在又有一个问题了,x节点的右子树原来是有子树B的,那么如果说现在y节点以及它的右子树(没有左子树,因为曾经x节点是它的左子树),放到了x节点的右子树上,那么岂不是多了什么吗?
我们知道 x节点的右子树必然是大于x节点的,然后y节点必然是大于x节点的右子树和x节点的,因为x节点和它的右子树都是在y节点的左子树,都比它小
既然如此的话,我们为什么不把x节点原来的右子树,放在节点y的左子树上面呢?这样的话,我们就巧妙地避开了冲突,达成了x节点上移,y节点下移.
移动后的图片
这就是一种情况,但是我们不能够局限一种情况,我们要找到通解.以下为通解
若节点x为y节点的位置z.( z为0,则为左节点,1则为右节点 )
\\1. 那么y节点就放到x节点的z^1的位置.(也就是,x节点为y节点的右子树,那么y节点就放到左子树,x节点为y节点左子树,那么y节点就放到右子树位置,这样就完美满足条件)
\\2. 如果说x节点的z^1的位置上,已经有节点,或者一棵子树,那么我们就将原来x节点z^1位置上的子树,放到y节点的位置z上面.(自己可以画图理解一下)
\\3. 移动完毕.
yyb大佬的代码,个人认为最精简的代码&最适合理解的代码.主要是因为注释多,不要打我脸
t是树上节点的结构体,ch数组表示左右儿子,ch[0]是左儿子,ch[1]是右儿子,ff是父节点
void update(int x)
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;//左子树+右子树+本身多少个,cnt记录重复个数.
void rotate(int x)//X是要旋转的节点
int y=t[x].ff;//X的父亲
int z=t[y].ff;//X的祖父
int k=t[y].ch[1]==x;//X是Y的哪一个儿子 0是左儿子 1是右儿子
t[z].ch[t[z].ch[1]==y]=x;//Z的原来的Y的位置变为X
t[x].ff=z;//X的父亲变成Z
t[y].ch[k]=t[x].ch[k^1];//X的与X原来在Y的相对的那个儿子变成Y的儿子
t[t[x].ch[k^1]].ff=y;//更新父节点
t[x].ch[k^1]=y;//X的 与X原来相对位置的儿子变成 Y
t[y].ff=x;//更新父节点
update(y);update(x);//yyb大佬忘记写了,这里是上传修改.
这里面用到了t[y].ch[1]==x
t[y].ch[1]
是y的右儿子,如果x是右儿子,那么这个式子是1,否则是0,也正好对应着左右儿子,同样的k^1,表示相对的儿子,左儿子0^1=1 右儿子1^1=0.其实上面我的文字们已经讲述清楚了,不过yyb大佬的代码,写得很好看这是真心话!!!
如果你已经读到了这里,那么恭喜你,现在的你成功完成了splay大部分了,但是你发现这条链表结构还是会卡死你,是不是感觉被耍了,不要气恼&气馁,因为你只需要再来一个splay函数就好了.你已经完成了85%,接下来的很容易的.
如果说x,y,z这三个节点共线,也就是x和它的父亲节点y和它的祖先节点在同一条线段上的话,那么我们就需要再来一些特殊处理了,其实就是一些很容易的操作.
下面就是三点共线的一张图片
这张图片里面的最长链是Z->Y->X->A
如果说我们一直都是x旋转的话,那么就会得到下面这张图片.
而一直旋转x的最长链是X->Z->y->B
我们发现旋转和不旋转似乎并没有任何区别,实际上也没有区别,也就是这些旋转操作后的树和不旋转的树,其实是没有多大区别的.
算法失败了,不过不用害怕,其实我们还有办法.
\\1. 如果当前处于共线状态的话,那么先旋转y,再旋转x.这样可以强行让他们不处于共线状态,然后平衡这棵树.
\\2. 如果当前不是共线状态的话,那么只要旋转x即可.
当你看懂这个以后恭喜你,你已经成功学会了splay的双旋操作了,然后你就可以看yyb大佬的代码了.
splay操作
void splay(int x,int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
int y=t[x].ff,z=t[y].ff;//父节点祖父节点
if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);//判断共线还是不共线
//这就是之前对于x和y是哪个儿子的讨论
rotate(x);//无论怎么样最后的一个操作都是旋转x
if(goal==0)root=x;//如果goal是0,则将根节点更新为x
查找find操作
从根节点开始,左侧都比他小,右侧都比他大,
所以只需要相应的往左/右递归
如果当前位置的val已经是要查找的数
那么直接把他Splay到根节点,方便接下来的操作
类似于二分查找,
所以时间复杂度O(logn)
inline void find(int x)//查找x的位置,并将其旋转到根节点
int u=root;
if(!u)return;//树空
while(t[u].ch[x>t[u].val]&&x!=t[u].val)//当存在儿子并且当前位置的值不等于x
u=t[u].ch[x>t[u].val];//跳转到儿子,查找x的父节点
splay(u,0);//把当前位置旋转到根节点
Insert操作
往Splay中插入一个数
类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数
如果不存在,在递归的查找过程中,会找到他的父节点的位置,
然后就会发现底下没有啦。。。
所以这个时候新建一个节点就可以了
inline void insert(int x)//插入x
int u=root,ff=0;//当前位置u,u的父节点ff
while(u&&t[u].val!=x)//当u存在并且没有移动到当前的值
ff=u;//向下u的儿子,父节点变为u
u=t[u].ch[x>t[u].val];//大于当前位置则向右找,否则向左找
if(u)//存在这个值的位置
t[u].cnt++;//增加一个数
else//不存在这个数字,要新建一个节点来存放
u=++tot;//新节点的位置
if(ff)//如果父节点非根
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;//不存在儿子
t[tot].ff=ff;//父节点
t[tot].val=x;//值
t[tot].cnt=1;//数量
t[tot].size=1;//大小
splay(u,0);//把当前位置移到根,保证结构的平衡
前驱/后继操作Next
首先就要执行Find操作
把要查找的数弄到根节点
然后,以前驱为例
先确定前驱比他小,所以在左子树上
然后他的前驱是左子树中最大的值
所以一直跳右结点,直到没有为止
找后继反过来就行了
inline int Next(int x,int f)//查找x的前驱(0)或者后继(1)
find(x);
int u=root;//根节点,此时x的父节点(存在的话)就是根节点
if(t[u].val>x&&f)return u;//如果当前节点的值大于x并且要查找的是后继
if(t[u].val<x&&!f)return u;//如果当前节点的值小于x并且要查找的是前驱
u=t[u].ch[f];//查找后继的话在右儿子上找,前驱在左儿子上找
while(t[u].ch[f^1])u=t[u].ch[f^1];//要反着跳转,否则会越来越大(越来越小)
return u;//返回位置
删除操作
现在就很简单啦
首先找到这个数的前驱,把他Splay到根节点
然后找到这个数后继,把他旋转到前驱的底下
比前驱大的数是后继,在右子树
比后继小的且比前驱大的有且仅有当前数
在后继的左子树上面,
因此直接把当前根节点的右儿子的左儿子删掉就可以啦
inline void Delete(int x)//删除x
int last=Next(x,0);//查找x的前驱
int next=Next(x,1);//查找x的后继
splay(last,0);splay(next,last);
//将前驱旋转到根节点,后继旋转到根节点下面
//很明显,此时后继是前驱的右儿子,x是后继的左儿子,并且x是叶子节点
int del=t[next].ch[0];//后继的左儿子
if(t[del].cnt>1)//如果超过一个
t[del].cnt--;//直接减少一个
splay(del,0);//旋转
else
t[next].ch[0]=0;//这个节点直接丢掉(不存在了)
第K大
从当前根节点开始,检查左子树大小
因为所有比当前位置小的数都在左侧
如果左侧的数的个数多余K,则证明第K大在左子树中
否则,向右子树找,找K-左子树大小-当前位置的数的个数
记住特判K恰好在当前位置
inline int kth(int x)//查找排名为x的数
int u=root;//当前根节点
if(t[u].size<x)//如果当前树上没有这么多数
return 0;//不存在
while(1)
int y=t[u].ch[0];//左儿子
if(x>t[y].size+t[u].cnt)
//如果排名比左儿子的大小和当前节点的数量要大
x-=t[y].size+t[u].cnt;//数量减少
u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
else//否则的话在当前节点或者左儿子上查找
if(t[y].size>=x)//左儿子的节点数足够
u=y;//在左儿子上继续找
else//否则就是在当前根节点上
return t[u].val;
以下为我自己编写的代码,其实就是yyb大佬的代码,只不过修改了一丢丢而已了.
#include <bits/stdc++.h>
using namespace std;
const int N=201000;
struct splay_tree
int ff,cnt,ch[2],val,size;
t[N];
int root,tot;
void update(int x)
t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
void rotate(int x)
int y=t[x].ff;
int z=t[y].ff;
int k=(t[y].ch[1]==x);
t[z].ch[(t[z].ch[1]==y)]=x;
t[x].ff=z;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].ff=y;
t[x].ch[k^1]=y;
t[y].ff=x;
update(y);update(x);
void splay(int x,int s)
while(t[x].ff!=s)
int y=t[x].ff,z=t[y].ff;
if (z!=s)
(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
rotate(x);
if (s==0)
root=x;
void find(int x)
int u=root;
if (!u)
return ;
while(t[u].ch[x>t[u].val] && x!=t[u].val)
u=t[u].ch[x>t[u].val];
splay(u,0);
void insert(int x)
int u=root,ff=0;
while(u && t[u].val!=x)
ff=u;
u=t[u].ch[x>t[u].val];
if (u)
t[u].cnt++;
else
u=++tot;
if (ff)
t[ff].ch[x>t[ff].val]=u;
t[u].ch[0]=t[u].ch[1]=0;
t[tot].ff=ff;
t[tot].val=x;
t[tot].cnt=1;
t[tot].size=1;
splay(u,0);
int Next(int x,int f)
find(x);
int u=root;
if (t[u].val>x && f)
return u;
if (t[u].val<x && !f)
return u;
u=t[u].ch[f];
while(t[u].ch[f^1])
u=t[u].ch[f^1];
return u;
void Delete(int x)
int last=Next(x,0);
int Net=Next(x,1);
splay(last,0);
splay(Net,last);
int del=t[Net].ch[0];
if (t[del].cnt>1)
t[del].cnt--;
splay(del,0);
else
t[Net].ch[0]=0;
int kth(int x)
int u=root;
while(t[u].size<x)
return 0;
while(1)
int y=t[u].ch[0];
if (x>t[y].size+t[u].cnt)
x-=t[y].size+t[u].cnt;
u=t[u].ch[1];
else if (t[y].size>=x)
u=y;
else
return t[u].val;
int main()
int n;
scanf("%d",&n);
insert(1e9);
insert(-1e9);
while(n--)
int opt,x;
scanf("%d%d",&opt,&x);
if (opt==1)
insert(x);
if (opt==2)
Delete(x);
if (opt==3)
find(x);
printf("%d\\n",t[t[root].ch[0]].size);
if (opt==4)
printf("%d\\n",kth(x+1));
if (opt==5)
printf("%d\\n",t[Next(x,0)].val);
if (opt==6)
printf("%d\\n",t[Next(x,1)].val);
return 0;
/*
插入数值x。
删除数值x(若有多个相同的数,应只删除一个)。
查询数值x的排名(若有多个相同的数,应输出最小的排名)。
查询排名为x的数值。
求数值x的前驱(前驱定义为小于x的最大的数)。
求数值x的后继(后继定义为大于x的最小的数)。
*/
作者:秦淮岸灯火阑珊
链接:https://www.acwing.com/activity/content/code/content/24072/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
感谢yyb大佬
注:我学会splay就是在yyb大佬的blog下成功的,然后成功地将它详细写了一遍,这道题目除了代码和一些图片,其他都是我写的,所以来说也算得上原汁原味的原著吧.
以上是关于Splay树详解的主要内容,如果未能解决你的问题,请参考以下文章