学习笔记 【平衡树 Splay】
平衡树 Splay
\(\text{Splay}\)是平衡树的一种,中文名为伸展树。
它的主要思想是:对于查找频率较高的节点,使其处于离根节点相对较近的节点。这样就可以保证了查找的效率。
旋转操作:
为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。
旋转需要保证:
整棵 \(\text{Splay}\) 的中序遍历不变(不能破坏二叉查找树的性质)。
受影响的节点维护的信息依然正确有效。
\(\text{root}\) 必须指向旋转后的根节点。
在 \(\text{Splay}\) 中旋转分为两种:左旋和右旋。

具体分析旋转步骤(假设需要旋转的节点为 \(x\))
- 设 \(y\) 为 \(x\) 的父亲, \(z\) 为 \(y\) 的父亲;
- \(z\) 的某一儿子为 \(y\) ,那么 \(z\) 的那个儿子变成 \(x\) ,\(x\) 的父亲就成了 \(z\) ;
- \(y\) 的某一儿子变成了 \(x\) 的另一儿子,相应的父亲改变;
- \(x\) 的另一儿子变成 \(y\) ,然后父亲改变。
inline void rotate(int x){
int y=fa[x],z=fa[y];
int k=son[y][1]==x;
son[z][son[z][1]==y]=x,fa[x]=z;
son[y][k]=son[x][k^1],fa[son[x][k^1]]=y;
son[x][k^1]=y,fa[y]=x;
pushup(y),pushup(x);//更新信息
}
Splay操作:
\(\text{Splay}\) 规定:每访问一个节点后都要强制将其旋转到根节点。
- 如果 \(x\) 的父亲是目标节点,直接将其旋转;
- 如果 \(x\) 的父亲不是目标节点,且 \(y\) 的儿子类型和 \(x\) 的儿子类型相同,先转 \(y\) ,再转 \(x\) ;
- 上述,如果儿子类型不同,先转两次 \(x\) 。
inline void splay(int x,int goal){
while(fa[x]!=goal){
int y=fa[x],z=fa[y];
if(z!=goal)
((son[y][1]==x)^(son[z][1]==y))?rotate(x):rotate(y);//类型相同?转x:转y
rotate(x);//再转x
}
if(!goal)root=x;//如果目标位置是根,那么根为x
}
插入操作:
沿用二叉查找树的方法,找到新节点应处的位置,新建节点后,再转到根。
inline void insert(int v){
int u=root,f=0;
while(u&&val[u]!=v)
f=u,u=son[u][v>val[u]];//找x位置
if(u) cnt[u]++;//若已有和x值一样的节点,那么个数++
else{
u=++tot;
if(f) son[f][v>val[f]]=u;//某一儿子
val[u]=v,fa[u]=f;//新建节点
siz[u]=cnt[u]=1;
}
splay(u,0);//转到根
}
找前驱后继:
首先我们通过 \(\text{find}\) 函数将要查的节点转到根。
inline void find(int v){
int u=root;
if(!u)return;
while(son[u][v>val[u]]&&v!=val[u])
u=son[u][v>val[u]];
splay(u,0);//转到根
}
看代码吧[捂脸]
inline int Nxt(int v,bool f){//f=0找前驱
find(v);
int u=root;
if(val[u]>v&&f)return u;//若val比v大且要找后继,直接返回u
if(val[u]<v&&!f)return u;//反之
u=son[u][f];
while(son[u][f^1]) u=son[u][f^1];//否则向某一方向子树的另一方向走到头。
return u;
}
查询一个数的排名:
分三种情况讨论:
- 找到了这个数,那么排名为左子树的大小;
- 当前节点值比 \(x\) 大那么递归进左子树;
- 当前节点值比 \(x\) 小那么递归进右子树,排名为他在右子树的排名加上左子树大小(包括重复)。
inline int arank(int u,int v){
if(!u)return 0;
if(v==val[u])return siz[son[u][0]];//case 1
if(v<val[u]) return arank(son[u][0],v);//case 2
return arank(son[u][1],v)+siz[son[u][0]]+cnt[u];//case 3
}
查询排名为x的数
也是分三种情况讨论:
- 当前节点左子树大小大于 \(rank\) ,递归进左子树;
- 当前节点左子树大小加上重复的个数大于 \(rank\) ,返回当前值;
- 否则递归进右子树,在右子树中,\(x\) 的排名为 \(rank\) -左儿子大小-重复个数。
inline int aval(int u,int rank){
if(siz[son[u][0]]>=rank) //case 1
return aval(son[u][0],rank);
if(siz[son[u][0]]+cnt[u]>=rank)//case 2
return val[u];
return aval(son[u][1],rank-siz[son[u][0]]-cnt[u]); //case 3
}
删除一个点
流程为:
- 找到这个点的前驱,后继;
- 将前驱转到根,将后继转到前驱下面;
- 那么此时后继的左儿子即为要删除的点。
inline void remove(int v){
int pre=Nxt(v,0);
int nxt=Nxt(v,1);//找前驱后继
splay(pre,0);splay(nxt,pre);//转
int del=son[nxt][0];
if(cnt[del]>1){//若有重复,减即可
cnt[del]--;
splay(del,0);还需转到根
}
else son[nxt][0]=0;//儿子清0
}
\(\text{Splay}\)的基本操作就完成了,后续添加相关题目。

浙公网安备 33010602011771号