Splay
Splay是一类二叉查找树,与其他平衡树相比,也是运用旋转保证复杂度
其最重要的操作便是
r
o
t
a
t
e
rotate
rotate
a
n
d
and
and
s
p
a
l
y
spaly
spaly了
先来谈旋转,我们都知道,旋转是这样的
仔细观察后,我们会发现,旋转操作可以拆解为三步,设
x
x
x是
y
y
y的父亲,
k
k
k表示
y
y
y是
x
x
x的儿子(左0右1),现在要将
y
y
y旋转
- 将 x x x的父节点的子节点从 x x x改为 y y y
- 将 x x x的 k k k儿子换成 y y y的 1 − k 1-k 1−k儿子
- 将
y
y
y的
1
−
k
1-k
1−k儿子变成
x
x
x
写成代码即为:
void rotate(int x){
int y=t[x].f,z=t[y].f,k=t[y].ch[1]==x;
t[y].ch[k]=t[x].ch[k^1];
t[t[x].ch[k^1]].f=y;
t[z].ch[t[z].ch[1]==y]=x;
t[x].f=z;
t[x].ch[k^1]=y;
t[y].f=x;
pushup(y);
pushup(x);//不要忘记旋转后更新信息
}
接着便是 S p l a y Splay Splay操作,对于使用到的一个节点 x x x,我们将其一直旋转至树根,便是 S p l a y Splay Splay操作,在旋转过程中,不能一直单旋转,我们发现 x 和 f a t h e r ( x ) x和father(x) x和father(x)都是自己父亲的 k k k儿子的情况下,先旋转 x x x的父节点,再旋转 x x x会使得树变得更加均衡,会更加优秀
void splay(int x,int goal=0){
while(t[x].f!=goal){
int y=t[x].f,z=t[y].f;
if(z!=goal){
t[z].ch[1]==y ^ t[y].ch[1]==x?rotate(x):rotate(y);
}
rotate(x);
}
if(!goal)root=x;
}
然后便是其他平衡树的常规操作了
- 插入 x x x 数
- 删除 x x x 数(若有多个相同的数,因只删除一个)
- 查询 x x x 数的排名(排名定义为比当前数小的数的个数 + 1 +1 +1 )
- 查询排名为 x x x 的数
- 求 x x x 的前驱(前驱定义为小于 x x x,且最大的数)
- 求 x x x 的后继(后继定义为大于 x x x,且最小的数)
我们来分别考虑几种操作,为了方便操作,我们往树中插入两个无穷大和无穷小的哨兵
1.插入操作
在树中查找
x
x
x,并记录其父节点
p
p
p,若找到了
x
x
x节点,则将其计数的
c
n
t
cnt
cnt加上1,若没有找到
x
x
x节点,此时
p
p
p一定是一个叶子节点,直接判断
x
x
x应是其的哪一个儿子,直接加上关系即可,最后还得
S
p
l
a
y
Splay
Splay一下
void insert(int x){
int u=root,p=0;
while(u&&t[u].val!=x){
p=u;
u=t[u].ch[x>t[u].val];
}
if(!u){
u=++tot;
t[u].cnt=t[u].siz=1;
t[u].f=p,t[u].val=x;
if(p)t[p].ch[x>t[p].val]=tot;
}
else{
t[u].cnt++;
}
splay(u);
}
2.删除操作
因为普普通通的删除很麻烦,所以我们采用一种简单的方式:
将
x
x
x在树中的严格前驱和严格后继找到,将严格前驱旋转到树根,将严格后继旋转为严格前驱的儿子,此时
x
x
x一定位于
t
[
t
[
r
o
o
t
]
.
c
h
[
1
]
]
.
c
h
[
0
]
t[t[root].ch[1]].ch[0]
t[t[root].ch[1]].ch[0],执行即可
void Delete(int x){
int last=nxt(x,0);
int nex=nxt(x,1);
splay(last,0);
splay(nex,last);
if(t[t[nex].ch[0]].cnt>1){t[t[nex].ch[0]].cnt--;splay(t[nex].ch[0]);}
else t[nex].ch[0]=0;
pushup(nex),pushup(last);
}
3.查询排名
我们可以对于每一个节点记录
s
i
z
siz
siz,表示节点
x
x
x及其子树中有多少个节点,这样我们就可以找到节点
x
x
x后将其旋转至树根,求树根左儿子的
s
i
z
siz
siz再加一即可,但由于哨兵无穷小的存在,无需加一
int rak(int x){
find(x);//查找到x所在节点,并将其旋转至根节点
return t[t[root].ch[0]].siz;
}
4.查找排名为
k
k
k的数
我们可以从根节点开始查找,对于一个节点
p
p
p,若
k
≤
t
[
t
[
p
]
.
c
h
[
0
]
]
.
s
i
z
k\le t[t[p].ch[0]].siz
k≤t[t[p].ch[0]].siz,那么这个节点就在
p
p
p的左子树中,若
t
[
t
[
p
]
.
c
h
[
0
]
]
.
s
i
z
<
k
≤
t
[
t
[
p
]
.
c
h
[
0
]
]
.
s
i
z
+
t
[
p
]
.
c
n
t
t[t[p].ch[0]].siz<k\le t[t[p].ch[0]].siz+t[p].cnt
t[t[p].ch[0]].siz<k≤t[t[p].ch[0]].siz+t[p].cnt,则说明当前节点
p
p
p即为要找的节点,否则令
k
=
k
−
t
[
p
]
.
c
n
t
−
t
[
t
[
p
]
.
c
h
[
0
]
]
.
s
i
z
k=k-t[p].cnt-t[t[p].ch[0]].siz
k=k−t[p].cnt−t[t[p].ch[0]].siz,然后再进入右子树
int kth(int u){
int x=root;
if(t[x].siz<u)return 0;
while(1){
if(ls&&u<=t[ls].siz){
x=ls;
}
else if(t[ls].siz+t[x].cnt<u){
u-=t[ls].siz+t[x].cnt;
x=rs;
}
else {splay(x);return t[x].val;}
}
}
5 and 6
前驱和后继的查找可以合成一段代码
x
x
x的前驱为
x
x
x成为根节点之后,左子树中最大的(即左子树的最右子节点(一直往右跳即可))
后继类似
int nxt(int x,int f){
find(x);
if ((f==0&&t[root].val<x)||(f==1&&t[root].val>x))return root;//特判x不在树中的情况
int u=t[root].ch[f];
while (t[u].ch[!f])u=t[u].ch[!f];
splay(u);
return u;
}
还有就是 f i n d find find函数
void find(int x){
int u=root;
while(t[u].ch[x>t[u].val]&&t[u].val!=x){
u=t[u].ch[x>t[u].val];
}
splay(u);
}