Splay伸展树(平衡树)(知识整理+板子总结)

心得

自己去年八月初学的时候,迷迷糊糊不怎么懂,

上学期学了数据结构AVL平衡树,现在再看才算完全明白

Splay平衡树, 理解了之后当板子用就可以了,

虽然还是要写一篇博客,万一哪天忘了之后想再学呢~

思路来源

https://www.cnblogs.com/cjyyb/p/7499020.html(知识点整理 yyb的博客)

https://blog.csdn.net/zhouzi2018/article/details/82456657(板子总结 洛谷P3369)

再附两篇有帮助的博客哈……

https://www.cnblogs.com/captain1/p/9733588.html

https://www.cnblogs.com/lykkk/p/10354301.html

还有一个写的不错的结构体Splay的板子,码住

https://www.cnblogs.com/qixingzhi/p/9365586.html

知识点

 

rotate函数:

Z是Y的父亲,Y是X的父亲,三者对应着四种旋转关系,

注意到旋转之后,X和Y的位置互换,

如果X是Y的左儿子,那么X的右儿子成为了Y的左儿子,其余不变

如果X是Y的右儿子,那么X的左儿子成为了Y的右儿子,其余不变

如果忽略Z-Y这一条链,那么就只需讨论两种情况,

实际上,Z-Y这一条链在rotate过程中也没有发生改变

然后就像链表那样暴力改一下父子关系就好了,

x==t[y].ch[1]代表x是不是y的右儿子,是就返回1,对应右儿子,不是返回0,对应左儿子

 

Splay函数:

其实对应AVL中单旋、双旋那种旋转过程,

Z、Y、X在一条链的情况,如上面画的图中的情况一和情况三,先旋转Y再旋转X

Z、Y、X不在一条链的情况,如上图情况二和情况四,旋转两次X

这样做是为了保证是一棵不退化的平衡树,不这样做的反例见思路来源

X没有祖父Z的情况,也就是Y是根,分X是Y的左子树和右子树两种情况,只旋转一次X

板子里对这六种情况作了高度精简的统一,好评

 

注意,splay伸展树的特殊操作为,

每对一个数进行操作,就把这个数所在的节点旋到树根

这样方便了后续对这个数进行的增删改查操作

 

find函数:

二叉搜索树正常操作,毕竟伸展树也是一棵二叉搜索树

如果想要大的就向右找,否则如果不等就向左找,等就返回

 

insert函数:

先像find函数一样左右找,找不到的话,会搜到叶子结点

如果叶子结点是要找的,就对其cnt加1,

否则新开一个节点存储当前值

每个节点内维护父亲ff,值val,当前节点这个值的数量cnt,和以这个点为根的子树大小siz

 

找值为x的前驱/后继:

先把要找的x旋到树根,如果x不存在,我们很有可能把其后继或前驱旋到了树根,

那么先判一下根节点是不是要找的后继/前驱,不是的话再往下找

前驱就去左子树里一路找右子树直至最右无叶子,

后继就去右子树里一路找左子树直至最左无叶子

 

Delete函数:

这个写法好评,比别的版本简单好多,

但要注意,主函数里先插入INF和-INF,否则只插入一个值的时候找不到前驱和后继

先找到前驱,把前驱旋到根节点,再找到后继,把后继旋到前驱的直接右子树

那么后继的左子树就是要删的值,且为叶子结点,直接删就好了

如果个数大于一个,直接cnt减1;否则直接令后继的左子树为0,代表不存在

 

查询第k大(增序中从前往后数第k个,其实是第k小):

和权值线段树的有点差别,毕竟当前节点还有个只属于自己的cnt,

先特判一下根节点的总个数siz是不是>=k的,如果不是,说明没这个排名,

如果左子树总个数siz>=k就去左子树找,

否则如果k>=左子树总个数siz+根节点个数cnt就去右子树找,

再否则,就是说明这个值在根节点里,就是根节点的值

 

然后就是注意一下这个借鉴的板子吧,

由于插入了-INF,本来排名为x的数变成了x+1

由于插入了-INF,本来值为x的最小排名 由 1+左子树总个数siz 变成了 左子树总个数siz

 

学会了知识之后,出门就是板子大法好啦,

毕竟不用像高中生(OI爷)一样把整个板子背下来

代码

#include<bits/stdc++.h>
using namespace std;
const int INF=2147483647;
const int maxn=1e5+10; 
inline int read()
{
    register int x=0,t=1;
    register char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-'){t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
    return x*t;
}
int root,tot;
struct Node
{
    int ch[2];//左右儿子
    int val;//值
    int ff;//父节点
    int size;//子树大小
    int cnt;//数字的数量
}t[maxn];
void pushup(int u)//下放操作
{
    t[u].size=t[t[u].ch[0]].size+t[t[u].ch[1]].size+t[u].cnt;
    //当前子树的大小是左子树大小加上右子树大小当前当前节点个数
}
void rotate(int x)//旋转操作
{
    int y=t[x].ff;//y是x的父节点
    int z=t[y].ff;//z是y的父节点
    int k=(t[y].ch[1]==x);//x是y的左儿子(0)还是右儿子(1)
    t[z].ch[t[z].ch[1]==y]=x;//把x旋转为z的儿子
    t[x].ff=z;//x的父亲更新为z
    t[y].ch[k]=t[x].ch[k^1];//把x的儿子给y
    t[t[x].ch[k^1]].ff=y;//更新父节点
    t[x].ch[k^1]=y;//y变为x的
    t[y].ff=x;//y的父亲更新为x
    pushup(y);pushup(x);//更新子节点数量
}
void splay(int x,int goal)//旋转操作,将x旋转为goal的儿子
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff;//x的父亲节点
        int z=t[y].ff;//x的祖父节点
        if(z!=goal)//如果z不是goal
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
            //如果x和y同为左儿子或者右儿子先旋转y
            //如果x和y不同为左儿子或者右儿子先旋转x
            //如果不双旋的话,旋转完成之后树的结构不会变化
        rotate(x);//再次旋转x,将x旋转到z的位置
    }
    if(goal==0)//如果目标位置是0,则是将x旋转到根节点的位置
        root=x;//更新根节点
}
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);//把当前位置移到根,保证结构的平衡
}
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);//把当前位置旋转到根节点
}
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;//返回位置
}
void Delete(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;//这个节点直接丢掉(不存在了)
}
int kth(int x)//查找排名为x的数 从小到大 
{
    int u=root;//当前根节点
    if(t[u].size<x)//如果当前树上没有这么多数
        return 0;//不存在
    while(1)
    {
        int y=t[u].ch[0];//左儿子
        if(x>t[y].size+t[u].cnt)
        //如果排名比左儿子的大小和当前节点的数量要大
        {
            x-=t[y].size+t[u].cnt;//数量减少
            u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
        }
        else//否则的话在当前节点或者左儿子上查找
            if(t[y].size>=x)//左儿子的节点数足够
                u=y;//在左儿子上继续找
            else//否则就是在当前根节点上
                return t[u].val;
    }
}
int n,op,x;
int main()
{
	scanf("%d",&n);
	//预先插入INF和-INF 便于处理前驱和后继
	//也使得Delete的写法不会导致死循环 
	insert(INF);
	insert(-INF);
	for(int i=1;i<=n;++i)
	{
		scanf("%d%d",&op,&x);
		if(op==1)insert(x);
		else if(op==2)Delete(x);
		else if(op==3)
		{
			//先将x所在的节点旋转到根 再询问左子树的大小 
			//本来rank=左size+1 由于插入-INF rank=现左size 
			find(x);
			printf("%d\n",t[t[root].ch[0]].size);
		}
		else if(op==4)printf("%d\n",kth(x+1));//由于插入-INF 所以询问排名x+1的值 
		else if(op==5)printf("%d\n",t[Next(x,0)].val);
		else if(op==6)printf("%d\n",t[Next(x,1)].val);
	}
	return 0;
}

附原博主代码(inline+read好评)

#include<bits/stdc++.h>
 
using namespace std;
 
const int MAX=500000;
 
inline int read()
{
    register int x=0,t=1;
    register char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-'){t=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
    return x*t;
}
 
int root,tot;
 
struct Node
{
    int ch[2];//左右儿子
    int val;//值
    int ff;//父节点
    int size;//子树大小
    int cnt;//数字的数量
}t[MAX];
 
inline void pushup(int u)//下放操作
{
    t[u].size=t[t[u].ch[0]].size+t[t[u].ch[1]].size+t[u].cnt;
    //当前子树的大小是左子树大小加上右子树大小当前当前节点个数
}
 
inline void rotate(int x)//旋转操作
{
    int y=t[x].ff;//y是x的父节点
    int z=t[y].ff;//z是y的父节点
    int k=(t[y].ch[1]==x);//x是y的左儿子(0)还是右儿子(1)
    t[z].ch[t[z].ch[1]==y]=x;//把x旋转为z的儿子
    t[x].ff=z;//x的父亲更新为z
    t[y].ch[k]=t[x].ch[k^1];//把x的儿子给y
    t[t[x].ch[k^1]].ff=y;//更新父节点
    t[x].ch[k^1]=y;//y变为x的
    t[y].ff=x;//y的父亲更新为x
    pushup(y);pushup(x);//更新子节点数量
}
 
inline void splay(int x,int goal)//旋转操作,将x旋转为goal的儿子
{
    while(t[x].ff!=goal)
    {
        int y=t[x].ff;//x的父亲节点
        int z=t[y].ff;//x的祖父节点
        if(z!=goal)//如果z不是goal
            (t[y].ch[0]==x)^(t[z].ch[0]==y)?rotate(x):rotate(y);
            //如果x和y同为左儿子或者右儿子先旋转y
            //如果x和y不同为左儿子或者右儿子先旋转x
            //如果不双旋的话,旋转完成之后树的结构不会变化
        rotate(x);//再次旋转x,将x旋转到z的位置
    }
    if(goal==0)//如果目标位置是0,则是将x旋转到根节点的位置
        root=x;//更新根节点
}
 
inline 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);//把当前位置移到根,保证结构的平衡
}
 
inline 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);//把当前位置旋转到根节点
}
 
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;//返回位置
}
 
inline void Delete(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;//这个节点直接丢掉(不存在了)
}
 
inline int kth(int x)//查找排名为x的数
{
    int u=root;//当前根节点
    if(t[u].size<x)//如果当前树上没有这么多数
        return 0;//不存在
    while(1)
    {
        int y=t[u].ch[0];//左儿子
        if(x>t[y].size+t[u].cnt)
        //如果排名比左儿子的大小和当前节点的数量要大
        {
            x-=t[y].size+t[u].cnt;//数量减少
            u=t[u].ch[1];//那么当前排名的数一定在右儿子上找
        }
        else//否则的话在当前节点或者左儿子上查找
            if(t[y].size>=x)//左儿子的节点数足够
                u=y;//在左儿子上继续找
            else//否则就是在当前根节点上
                return t[u].val;
    }
}
 
int main()
{
    int n=read();
    insert(+2147483647);
    insert(-2147483647);
    while(n--){
        int opt=read();
        if(opt==1){
            int x=read();
            insert(x);
        }
        if(opt==2){
            int x=read();
            Delete(x);
        }
        if(opt==3){
            int x=read();
            find(x);
            printf("%d\n",t[t[root].ch[0]].size);
        }
        if(opt==4){
            int x=read();
            printf("%d\n",kth(x+1));
        }
        if(opt==5){
            int x=read();
            printf("%d\n",t[Next(x,0)].val);
        }
        if(opt==6){
            int x=read();
            printf("%d\n",t[Next(x,1)].val);
        }
    }
    return 0;
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值