ni_ju_ge 的 Splay 学习笔记
前言
书接上回,我们来学习要转来转去的平衡树—— Splay 树。
旋转
左旋
左旋拎右左挂右,即
代码实现:
void lturn(int &pos) {
int ri=tree[pos].r;
tree[pos].r=tree[ri].l;//左挂右
tree[ri].l=pos;//拎右
pos=ri;//还是拎右
up(tree[pos].l);
up(pos);
}
右旋
右旋拎左右挂左,就是复读机。
代码实现:
void rturnzag(int &pos) {
int le=tree[pos].l;
tree[pos].l=tree[le].r;
tree[le].r=pos;
pos=le;
up(tree[pos].r);
up(pos);
}
Splay
提问:旋转很多次的叫什么?
回答:那维莱特 Splay 树!
提问二:有一个退化成链的二叉查找树,从根节点开始分别为
回答:没错,它还是一条链。
提问三:那旋转还有什么用?
回答:装 B! 可以用双旋!
双旋
前面的左旋(zag)和右旋(zig)都是单旋,两次单旋就是双旋操作了,双旋操作能有效打乱二叉搜索树。
Splay 树有以下四种双旋操作:
zig-zig
在
zag-zag
其实就是 zig-zig 的复读机。
zag-zig
在
zig-zag
其实就是 zag-zig 的复读机。
存储
简单的 Splay 存左子节点、右子节点、数据、子树大小和重复次数(可以不存,但要多维护一个父亲节点)即可。
struct node {
int l,r,dat,size,same;
} tree[100000];
int root,cnt;
void make(int &pos,int val) {
tree[++cnt].dat=val;
tree[cnt].size=1;
tree[cnt].same=1;
pos=cnt;
}
伸展
Splay 很重要的操作就是伸展。即将某个节点旋转到根节点的操作。
我代码里的伸展是递归实现的,常数比较大,但不递归的要维护父节点,难度比较大,只能说鱼和熊掌不可兼得 XD。
void splay(int x,int &y) {
if(x==y)return;
int &l=tree[y].l,&r=tree[y].r;
if(x==l)zig(y);
else if(x==r)zag(y);
else {
if(tree[x].dat<tree[y].dat) {
if(tree[x].dat<tree[l].dat)splay(x,tree[l].l),zig(y),zig(y);
else splay(x,tree[l].r),zag(l),zig(y);
} else {
if(tree[x].dat<tree[r].dat)splay(x,tree[r].l),zig(r),zag(y);
else splay(x,tree[r].r),zag(y),zag(y);
}
}
}
插入
与二叉搜索树一样,不过要注意,插入后还要伸展到根节点。
void take(int &pos,int val) {
if(pos==0)make(pos,val),splay(pos,root);
else if(val<tree[pos].dat)take(tree[pos].l,val);
else if(val>tree[pos].dat)take(tree[pos].r,val);
else tree[pos].same++,tree[pos].size++,splay(pos,root);//值相同,重复次数和子树大小增加
}
删除
找到该节点,将其伸展到根节点。
- 若其重复个数不止一个,直接从里面减去就行。
- 否则若其没有右子节点,直接将 root 替换为它的左子节点
- 否则找到它的后缀,将后缀伸展到它的右子节点,然后将右子节点的左子节点变为它的左子节点,然后将 root 改为它的右子节点(如果看不懂,就再看一遍)
void del(int val,int pos) {
if(pos==0)return;
else if(val<tree[pos].dat)del(val,tree[pos].l);
else if(val>tree[pos].dat)del(val,tree[pos].r);
else {
splay(pos,root);
if(tree[root].same!=1)tree[root].same--,tree[root].size--;
else if(tree[root].r==0)root=tree[root].l;
else {
pos=tree[root].r;
while(tree[pos].l!=0)pos=tree[pos].l;//找到它的后缀
splay(pos,tree[root].r);//将后缀伸展到它的右子节点
tree[tree[root].r].l=tree[root].l;//将右子节点的左子节点变为它的左子节点
root=tree[root].r;//将 root 改为它的右子节点
up(root);
}
}
}
查询排名
将二叉搜索树的改一下就行,注意最后还要伸展一下。
int wrank(int val) {
int pos=root,rnk=1;
while(pos) {
if(tree[pos].dat==val) {
rnk+=tree[tree[pos].l].size;
splay(pos,root);
break;
}
if(val<=tree[pos].dat)
pos=tree[pos].l;
else {
rnk+=tree[tree[pos].l].size+tree[pos].same;
pos=tree[pos].r;
}
}
return rnk;
}
查询数字
同样改一下二叉搜索树的。
int num(int val) {
int pos=root;
while(pos) {
int l=tree[tree[pos].l].size;
if(l<val&&val<=l+tree[pos].same) {//注意这里判断它是否在区间内
splay(pos,root);
break;
} else if(l>=val)pos=tree[pos].l;
else {
val-=l+tree[pos].same;
pos=tree[pos].r;
}
}
return tree[pos].dat;
}
前缀与后缀
有了查询排名和查询数字,只要充分发扬人类智慧,有它俩来实现这俩即可。自行理解。
int pre(int val) {
return num(wrank(val)-1);
}
int last(int val) {
return num(wrank(val+1));
}
后记
笑点解析:模版题P3369中,Treap 和 Splay 的时间均在 475 ms 左右,但普遍认为 Splay 比 Treap 快。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?