Splay入门
Posted bigyellowdog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Splay入门相关的知识,希望对你有一定的参考价值。
Splay入门
- write by:BigYellowDog
- 大部分资料整理于小蒟蒻yyb的博客
引入:
- 首先要学习Splay之前,你需要知道Splay的前世今生!
- 世上有这么个东西,叫平衡树。它的定义是对于任意一个节点,左儿子的值比它小,右儿子的值比它大
并且任意一棵子树单独拎出来也是一棵平衡树。如下图:
Look at this!这就是一棵平衡树。
- 它现在很平衡,是一棵满二叉树,高度正好是logn
如果这棵树极端一点,它就会变成这样 ↓
- 现在看起来,这个东西一点都不平衡
- 但其实它只是退化成了一条链
- 所以这时如果要查询的话,最坏情况下就变成了O(n)
- 这就很难受了!
- 所以为了解决这个难题。各路大神们就弄出了各种树来解决问题。而我们的Tarjan大佬就弄出了Splay这玩意
- Splay就这样产生了!
原理的探究:
- 前文中提到了Splay的产生是因解决平衡树尴尬的查询问题。那么它的实现原理又是什么呢?
顾名思义,Splay译为 “伸展、旋转”
所以它的原理就是:每次查询都会调整树的结构,使被查询频率高的条目更靠近树根。
那么,如何旋转呢?我们先做一个简单的旋转模拟:
任务:将X旋转到Y的位置,并保持这个东西还是一棵平衡树
思路:
- 通过观察,我们发现这样的关系:
Y < Z
X < Y < C
A < X < B
嗯... ...通过上述关系,我们可以通过人脑画出旋转后的图,问题解决(图如下)
我们检查一下,原来的大小关系为:
A < X < B < Y < C < Z
旋转之后大小关系为:
A < X < B < Y < C < Z
诶,大小关系也没有变。所以之前那棵平衡树就可以通过旋转变成这个样子,并且这个时候还是一棵平衡树
注意!前文说了,这只是一个简单的旋转模拟。在实际中,X和Y的位置不只这些,有以下几种情况:
- 有Y是Z的左儿子 X是Y的左儿子
- 有Y是Z的左儿子 X是Y的右儿子
- 有Y是Z的右儿子 X是Y的左儿子
- 有Y是Z的右儿子 X是Y的右儿子
那么现在,我们就来正式探究Splay的一般情况:
从古至今,解决一般性问题都是从特殊性中解决。所以还是上面那个图,我们可以发现这些规律:
- Y是Z的哪个儿子,旋转之后X就是Z的哪个儿子
- X是Y的哪个儿子,那么旋转完之后,X的那个儿子就不会变
- 证明:
- 如果X是Y的左儿子,A是X的左儿子
那么A < X < Y旋转完之后A还是X的左儿子
如果X是Y的右儿子,A是X的右儿子
那么A > X > Y 只是把不等式反过来了而已
- 如果原来X是Y的哪一个儿子,那么旋转完之后Y就是X的另外一个儿子
- 证明:
- 如果X是右儿子 X > Y,所以旋转后Y是X的左儿子
如果X是左儿子 Y > X,所以旋转后Y是X的右儿子
- 把每个规律看懂,自己再理理思路,还会发现一个规律:
Z | 位置始终不变 |
---|---|
X、Y | 相互交换位置 |
B | 原位置改变了 |
A、C | 与自己爸爸的相对位置不变 |
- 于是“旋转”的代码就顺理成章的得出:
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; //更新父节点
}
- 好了如果你坚持看到这里。恭喜你,Splay的原理你看懂了!
细谈“旋转”:
- 通过前文我们了解到Splay为了解决O(n)查询的问题。使用到了“旋转”这一操作。但实际中旋转不仅仅那么简单,它还有一些约束条件。我们继续来学习
- 现在考虑一个问题,如果要把一个节点旋转到根节点(比如上面的Z节点)。我们是不是可以做两步,先把X转到Y再把X转到Z呢?
- 好,我们试试看
- 如下,将X旋转到Y位置的时候:
- 再接着把X旋转到Z之后:
好了,旋转完了。看起来似乎没啥毛病。
真的没问题吗?原图中有一条瓜皮的链: Z->Y->X->B
- 当我们旋转完后,发现这条瓜皮的链还在... ...
- 也就是说,如果你只对X进行旋转的话,有一条链依旧存在,如果是这样的话,splay很可能会被卡。
- (PS:这里笔者自己都有点懵,但笔者的理解是这样旋转并没有满足“查询频率高的条目更靠近树根”的宗旨)
所以,我们可以发现:对于XYZ的不同情况,有不同的旋转方式!
那么这里我直接把这几种情况总结了起来:
- 第一种,X和Y分别是Y和Z的同一个儿子
- 对于情况一,也就是类似上面给出的图的情况,就要考虑先旋转Y再旋转X
- 第二种,X和Y分别是Y和Z不同的儿子
- 对于情况二,就是对X旋转两次,先旋转到Y再旋转到Z
- 不存在Z节点,也就是Y节点就是Splay的根
- 此时无论怎么样都是对于X向上进行一次旋转
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 }
- 第一种,X和Y分别是Y和Z的同一个儿子
基本操作:
- find操作
- insert操作
- eraser操作(学这个前先学第4点)
- 前驱/后继操作
- 合并操作
- 分离操作
- 第K大操作
- 是不是看到这些想砸键盘。没错,默默接受把Orz。我们一个一个来看
1. find操作
- 步骤:从根节点开始,左侧都比他小,右侧都比他大。所以只需要相应的往左/右递归。如果当前位置的val已经是要查找的数,那么直接把他Splay到根节点,方便接下来的操作。
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);//把当前位置旋转到根节点
}
2. insert操作
- 步骤:类似于Find操作,只是如果是已经存在的数,就可以直接在查找到的节点的进行计数。如果不存在,在递归的查找过程中,会找到他的父节点的位置,然后就会发现底下没有,所以这个时候新建一个节点就可以了
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);//把当前位置移到根,保证结构的平衡
}
3. eraser操作
- 步骤:首先找到这个数的前驱,把他Splay到根节点,然后找到这个数后继,把他旋转到前驱的底下。比前驱大的数是后继,在右子树比后继小的且比前驱大的有且仅有后继的左儿子。因此直接把当前根节点的后继的左儿子删掉就可以了
void eraser(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;//这个节点直接丢掉(不存在了)
}
4. 前继/后继操作
- 步骤:首先就要执行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;//返回位置
}
- 还有几个操作没有写。今天就先写到这里了(2018.12.4)
以上是关于Splay入门的主要内容,如果未能解决你的问题,请参考以下文章