Splay伸展树是一种特殊的BST,Splay能在Log(n)的均摊时间复杂度内完成插入、查找,删除等基本操作。
基本结构
struct SplayTree
{
int val,sz,cnt;//val为值 sz为子树大小,cnt此位置的重叠节点个数
int s[2],f;//s[0]为左儿子 s[1]为右儿子 f为父亲
};
旋转
1.左右儿子判断:如果 当前节点 为 当前节点的父节点 的左儿子,返回0,若右儿子则返回1
bool son(int x)
{
return a[a[x].f].s[1] == x;
}
2.将y节点插入到x节点的下面:z为0插入左儿子的位置,z为1插入右儿子的位置
void insert(int x , int y , int z)
{
a[x].s[z] = y;
a[y].f = x;
}
3.旋转(rotate)
左旋(当前节点为父节点的左孩子)
①将祖父节点与自身链接
②用自身的右孩子代替的自身父亲的左节点
③用自身的父节点代替自身的右孩子
右旋(当前节点为父节点的右孩子)
①将祖父节点与自身链接
②用自身的左孩子代替的自身父亲的右节点
③用自身的父节点代替自身的左孩子
左旋(图片来自oi.men.ci):
旋转操作可以通过 异或运算 轻松实现
void rot(int x)
{
int p = a[x].f;
bool d = son(x);
insert(a[p].f,x,son(p));//第一步
insert(p,a[x].s[d^1],d);//第二步
insert(x,p,d^1);//第三步
a[p].sz = a[a[p].s[0]].sz + a[a[p].s[1]].sz + a[p].cnt;//重新计算子树大小
a[x].sz = a[a[x].s[0]].sz + a[a[x].s[1]].sz + a[x].cnt;//重新计算子树大小
if(a[x].f == 0)
root = x;//root是根节点的编号
}
Splay如何实现平衡
Spaly每插入一个新节点,就将新插入的节点旋转到根节点
1.如果当前点是根节点,则直接结束。
2.如果当前点的父亲是根节点,直接将节点旋转到根节点
3.如果上述两条都不满足,设x为当前节点
①利用son()函数,得出son(x)和son(x.f)
②若son(x) == son(x.f) 那么先rotate(x.f) 然后 rotate(x)
③若son(x) != son(x.f) 那么先rotate(x) 然后重复执行一次 rotate(x)
void splay(int x)
{
while(a[x].f != 0)
{
if(a[a[x].f].f == 0)
rot(x);
else
{
if(son(x) == son(a[x].f))
{
rot(a[x].f);
rot(x);
}
else
{
rot(x);
rot(x);
}
}
}
}
插入
Spaly是平衡的BST,所以插入操作和普通BST相同。
每次插入完成,将新插入的节点Rotate到根节点
void ins(int x)
{
int w = root,f = 0;
int p = findn(x);
if(p)//如果这个值已经存在,那么就直接给这个节点+1吧
{
a[p].cnt ++;
while(p != root)
{
a[p].sz = a[a[p].s[0]].sz + a[a[p].s[1]].sz + a[p].cnt;//重新计算子树大小
p = a[p].f;
}
a[root].sz = a[a[root].s[0]].sz + a[a[root].s[1]].sz + a[root].cnt;//重新计算子树大小
return ;
}
while(w)
{
f = w;
if(x < a[w].val)
w = a[w].s[0];
else
w = a[w].s[1];
}
a[++tot].val = x;
a[tot].cnt = 1;//新建节点
if(f == 0)//如果这个点是根节点的话,直接插入
{
root = tot;
rejs(root);
return ;
}
//////////// ↑ ↑
//普通BST的插入 ↑ ↑
//////////// ↑ ↑
///////////////↓ ↓
//实现平衡的操作↓ ↓
///////////////↓ ↓
if(x < a[f].val)
point(f,tot,0);
else
point(f,tot,1);
splay(tot);
}
查找
查找操作与普通BST相同
bool Find(int pos , int x)//查找的大概逻辑
{
if(a[pos].size == 0) return false;
if(a[pos].val == x) return true;
if(x > a[pos].val) return Find(a[pos].s[1] , x);
else return Find(a[pos].s[0] , x);
}
排名
一个值在BST中的排名就是二叉树中序遍历的时候,该值第一次出现的位置。我们可以一次中序遍历求出所有值得排名。
在Splay中,一个值的排名,就是将该值所在的节点Rotate()到根节点,此时其左子树的size+1就是排名
查询排名
与普通BST类似
节点的前驱和后继
前驱:中序遍历中第一个小于该节点的值的节点
后继:中序遍历中第一个大于该节点的值得节点
求节点的前驱:
①找左子树中最大的,若左子树为空,则执行②
②向上查找,若当前节点是上一个节点的右子树,则返回当前节点的父亲
③若查找到根节点,则代表没有前驱
求节点的后继
①找右子树中最小的,若右子树为空,则执行②
②向上查找,若当前节点是上一个节点的左子树,则返回当前节点的父亲
③若查找到根节点,则代表没有后继
int near(int x,bool d)//0代表求前驱;1代表后继
{
if(a[x].s[d] != 0){
int p = a[x].s[d];
while(a[p].s[d^1])
p = a[p].s[d^1];
return p;
}
else if(son(x) == d^1)
return a[x].f;
else{
int p = a[x].f;
while(son(p) == d){
if(p == root)
return 0;
p = a[p].f;
}
return a[p].f;
}
}
值的前驱和后继
若欲求前驱和后继的值在BST中,直求节点的前驱和后继。
若不在,首先插入一个值与欲求前驱后继的值相等的节点,然后求节点的前驱和后继,求完之后将插入的节点删除。
删除
首先Splay(x的前驱) , 然后Splay(x的后继) , 完成这两个操作后x的后继会变成根节点,x的前驱会变成根节点的左儿子,x变成了根节点的右子树。删除右子树,就删除了x节点。
删除区间的时候也可以这样:首先Splay(区间左边界的前驱) , 然后Splay(区间右边界的后继),操作完成后整个区间就变成了 区间左边界前驱的右子树。
void cle(int x)//cle操作是清除一个点
{
a[a[x].f].s[son(x)] = 0;
a[x].val = 0;
a[x].sz = 0;
rejs(a[x].f);
a[x].f = 0;
}
void del(int x)
{
int p = near(x,0);
int q = near(x,1);
if(!p || !q)
{
splay(x);
if(p == 0)
{
root = a[x].s[1];
a[a[x].s[0]].f = 0;
}
else
{
root = a[x].s[0];
a[a[x].s[0]].f = 0;
}
return ;
}
splay(p);
splay(q);
rot(p);
cle(x);
}