Splay入门详解

article/2025/11/9 15:48:55

Splay入门详解

写在前面

听说平衡树是一种强大的数据结构,听同年级或高年级大佬们讲起来也感觉很牛笔的亚子,而最近XC又叫我们去学习一下 L C T LCT LCT!?
又因为 S p l a y Splay Splay是学习 L C T LCT LCT的基础,而且又比较脍炙人口,于是我便学了一下,经过一个白天的努力,也终于是学会了一点皮毛。
为了加深我对 S p l a y Splay Splay的理解,把网上一些讲得有点模糊的知识点给没看懂的同学讲解清楚,于是我就写了这篇博客,保证大家都可以入门 (不行也别怪我哦)

Splay是神马东东?

给出百度的解释: S p l a y Splay Splay
下面我用简单易懂的语言来简单介绍一下:
首先 S p l a y Splay Splay是一棵二叉查找树,之后 S p l a y Splay Splay又可以通过各种骚操作(主要是旋转操作)使得整棵树仍然满足二叉查找树的性质,并且保持相对的平衡不至于退化成链而大大提高复杂度,这个神奇的东西是由Daniel Sleator 和 Robert Tarjan这两位大神发明的
二叉查找树又是神马呢?
首先他肯定是一棵二叉树 (废话)
而且这棵树有一个非常厉害的性质:能够在这棵树上查询到的节点一定满足:左子树任意节点的值 < < <根节点的值 < < <右子树任意节点的值

例题 LOJ #104.普通平衡树

这道题的题目大意就是有n个操作,一共有6种操作,对应1~6编号:
1.插入 x x x
2.删除 x x x数(若有多个相同的数,因只删除一个)
3.查询 x x x数的排名(排名定义为比当前数小的数的个数+1)
4.查询排名为 x x x的数
5.求 x x x的前驱(前驱定义为小于 x x x,且最大的数)
6.求 x x x的后继(后继定义为大于 x x x,且最小的数)
之后对于3,4,5或6操作的结果进行输出

详解Splay操作

接下来我们来结合我的解析和代码对Splay的操作进行了解

变量

int n;//操作个数
int son[N][2];//该节点的左/右儿子的编号,0表示左儿子,1表示右儿子
int fa[N];//该父亲节点
int cnt;//节点的总数
int tot[N];//该节点权值出现的次数
int size[N];//以该节点为根的子树大小
int root;//根节点
int val[N];//该节点的权值

三个最基本的操作

  • 改变节点在树中的位置之后,更新该节点的 s i z e size size值: m a i n t a i n ( x ) maintain(x) maintain(x)
inline void maintain(int x) {size[x]=size[son[x][0]]+size[son[x][1]]+tot[x];}
//左子树的大小加上右子树的大小在加上该节点的值的个数
  • 判断该节点是父亲的左儿子还是右儿子: g e t ( x ) get(x) get(x)
inline bool get(int x) {return x==son[fa[x]][1];}
//如果跟右儿子相等则返回1,否则返回0
  • 销毁该节点: c l e a r ( x ) clear(x) clear(x)
inline void clear(int x) {size[x]=fa[x]=son[x][0]=son[x][1]=tot[x]=val[x]=0;}
//该节点的所有数据全部清除

旋转操作 rotate(x)

这个操作是为了使 S p l a y Splay Splay保持平衡而产生的,其本质是将某个节点上移一个位置
旋转操作需要保证:

  • 整棵 S p l a y Splay Splay 中序遍历(就是先遍历左儿子,再遍历根节点,最后遍历右儿子) 的顺序不变(为了不破坏二叉查找树的性质)
  • 旋转后受到影响的节点所维护的信息仍然是正确有序的
  • r o o t root root必须变成旋转过后的根节点

S p l a y Splay Splay中的旋转分为左旋右旋两种,如图所示:
在这里插入图片描述
我们来分析一下旋转具体操作(这里以右旋为例子,假设需要旋转的节点为 x x x,其父亲为 y y y,如果是左旋则所有方向相反):

  1. x x x的右儿子交给 y y y当它的左儿子,将 x x x的右儿子的父亲赋值为 y y y
  2. y y y赋值为 x x x的右儿子,将 y y y的父亲赋值为 x x x
  3. 如果 y y y存在父亲 z z z,那么 y y y原来所在的 z z z的儿子的位置,赋值为 x x x,将 x x x的父亲赋值为 z z z

我们可以看到这棵 S p l a y Splay Splay左旋右旋怎么旋它的中序遍历顺序还是:42513
通过分析我们可以发现,如果 x x x左儿子则右旋,如果是右儿子则左旋,于是我们可以先处理出一个参数k表示 x x x是左儿子还是右儿子,在运用位运算异或来进行旋转

inline void rotate(int x)
{int y=fa[x],z=fa[y],k=get(x);//y是x的父亲,z是y的父亲,k表示x是左儿子还是右儿子son[y][k]=son[x][k^1];fa[son[x][k^1]]=y;//步骤1son[x][k^1]=y;fa[y]=x;//步骤2fa[x]=z;if (z) son[z][son[z][1]==y]=x;//步骤3maintain(x);maintain(y);//不要忘记了更新size值哦
}

Splay操作(重中之重)Splay(x)

此操作是 S p l a y Splay Splay最特别又是最重要的操作,因为 S p l a y Splay Splay规定了:每访问一个节点后都要强制将其旋转到根节点,而此旋转操作分以下种情况(假设 x x x是需要旋转到根节点的节点),如图所示:
在这里插入图片描述

  • 如果 x x x父亲是根节点(如图1,2),则直接将 x x x左旋或右旋即可
  • 如果 x x x父亲不是根节点(如图3,4), x x x与父亲的类型相同(同为左儿子或右儿子),则先将其父亲左旋或右旋,再将 x x x右旋或左旋
  • 如果 x x x父亲不是根节点(如图5,6),且 x x x与父亲的类型不同,则 x x x左旋再右旋,或右旋再左旋
inline void splay(int x)
{int y=fa[x];//先找到x节点的父亲ywhile (y)//如果x不是根节点(即x的父亲不为0){if (fa[y])//如果x的父亲不是根节点{if (get(x)==get(y)) rotate(y);//如果x与父亲的类型相同,则旋转父亲节点else rotate(x);//否则旋转x}rotate(x);//旋转xy=fa[x];//更新父亲节点}root=x;//更新根节点
}

大家最好自己画图模拟一下以上6种情况,以充分掌握 S p l a y Splay Splay操作,因为这个操作真的非常重要

插入操作

此操作是一个代码比较长比较复杂的操作,假设我们需要将数 k k k插入 S p l a y Splay Splay中,具体操作如下:

  • 如果此时树是空树,那么直接插入根节点退出即可
  • 如果当前节点的权值等于 k k k,那么就增加该节点的大小更新当前节点的 s i z e size size值和父亲节点的 s i z e size size,之后将该节点 S l p a y Slpay Slpay操作旋转到根
  • 否则就按照二叉查找树的性质往下找,找到空节点直接插入,之后将该节点 S p l a y Splay Splay操作旋转到根
inline void ins(int k)
{if (!root)//如果是空树(即整棵树没有根){val[++cnt]=k;++tot[cnt];root=cnt;//直接插入到根节点maintain(root);return;//更新size值后退出}int x=root,y=0;//否则从根节点往下寻找,y表示当前节点x的父亲while (1){if (val[x]==k)//如果当前节点的权值等于k{++tot[x];maintain(x);maintain(y);//更新权值数量,更新当前节点和父亲节点的size值splay(x);break;//Splay到根然后退出}y=x;x=son[x][val[x]<k];//继续往下找if (!x)//找到一个空节点{val[++cnt]=k;++tot[cnt];fa[cnt]=y;son[y][val[y]<k]=cnt;//直接插入maintain(cnt);maintain(y);//更新当前节点和父亲的size值splay(cnt);break;//Splay到根然后退出}}
}

查询排名(以从小到大排名为例)rk(k)

假设我们需要查询数 k k k在Splay中的排名,我们可以根据二叉查找树的性质使用如下操作进行查询:

  • 如果 k k k比当前节点的权值小,则向其左子树查找
  • 如果 k k k比当前节点的权值大,则将答案加上左子树的大小和当前节点的权值数量,往右子树查找
  • 如果 k k k等于当前节点的权值,则将答案加一然后返回即可
  • 最后不要忘记将查询到等于 k k k的权值的节点 S p l a y Splay Splay到根
inline int rk(int k)
{int res=0,x=root;//从根节点开始查找while (1){if (k<val[x]) x=son[x][0];//如果k小于当前节点的权值,则往左子树查找else{res+=size[son[x][0]];//否则答案加上左子树的大小if (k==val[x]) {splay(x);return res+1;}//如果当前节点的权值等于k,则Splay到根后答案加一返回res+=tot[x];x=son[x][1];//否则即是比k小,则答案加上当前节点权值个数往右子树查询}}
}

区间第k小/大(这里以区间第k小为例)kth(k)

假设 k k k为当前剩余排名,我们同样可以根据二叉查找树的性质进行查找,操作如下:

  • 如果左子树不为空树且左子树大小大于等于 k k k,则往左子树进行查找
  • 否则 k k k减去左子树和根节点的大小。如果k小于等于0,则返回根节点的权值,否则往右子树继续查找
inline int kth(int k)
{int x=root;//从根节点开始查找while (1){if (son[x][0] && k<=size[son[x][0]]) x=son[x][0];//如果左子树不为空树且左子树大小大于等于k,则往左子树进行查找else{k-=size[son[x][0]]+tot[x];//否则将k减去左子树和根节点的大小if (k<=0) {splay(x);return val[x];}//如果k小于等于0,则Splay到根后返回根节点的权值x=son[x][1];//否则往右子树继续查找}}
}

查询k的前驱 pre()

k k k的前驱是小于 k k k中的所有数最大的那个,操作很简单:
先把 k k k插入进 S p l a y Splay Splay(此时 k k k已经被 S p l a y Splay Splay到根节点了),然后前驱即为左子树中最大的那个数即左子树中最靠右的节点再删除 k k k

inline int pre()
{int x=son[root][0];//先查找到左子树while (son[x][1]) x=son[x][1];//不停往右子树查找splay(x);return x;//找到了,Splay到根后返回
}

查询k的后继 nxt()

k k k的后继是大于 k k k中的所有数中最小的那个,操作很查询前驱很类似,就不多说了

inline int nxt()
{int x=son[root][1];//先查找到右子树while (son[x][0]) x=son[x][0];//不停往左子树查找splay(x);return x;//找到了,Splay到根后返回
}

删除操作 del(k)

此操作也是一个代码比较长比较复杂的操作,具体操作如下:
首先 k k k旋转到根的位置(用一个rk操作就行了),然后

  • 如果权值出现次数大于1,则将权值出现次数减一就行了
  • 否则合并当前节点的左右子树

而合并两棵 S p l a y Splay Splay的规则是(假设要合并根节点为 x x x y y y的两棵 S p l a y Splay Splay):

  • 如果 x x x y y y其中之一是空树或者都是空树,则返回非空树的根节点或者空树
  • 否则将 x x x子树中的最大值 S p l a y Splay Splay到根,再以这个节点为根节点,将 y y y赋值为它的右子树,更新信息再返回

很显然这仍然符合 S p l a y Splay Splay的性质

inline void del(int k)
{rk(k);//将节点k旋转到根节点if (tot[root]>1) {--tot[root];maintain(root);return;}//权值出现次数大于1,则减一返回if (!son[root][0] && !son[root][1]) {clear(root);root=0;return;}//两棵空树,即销毁根节点返回if (!son[root][0]) {int x=root;root=son[x][1];fa[root]=0;clear(x);return;}if (!son[root][1]) {int x=root;root=son[x][0];fa[root]=0;clear(x);return;}//其中一个为空树,直接返更新信息,返回非空树int x=root,pre1=pre();//否则把左子树中最大的找出来(即跟的前驱)fa[son[x][1]]=pre1;son[pre1][1]=son[x][1];//将节点x的右子树赋值为yclear(x);maintain(root);//销毁节点x,更新根节点信息
}

总CODE(是以上代码的整合压行版)

#include<cstdio>
#include<string>
#define R register int
#define N 100005
using namespace std;
int n,son[N][2],fa[N],cnt,tot[N],size[N],root,val[N];
inline void read(int &x)
{x=0;int f=1;char ch=getchar();while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();x*=f;
}
inline void maintain(int x) {size[x]=size[son[x][0]]+size[son[x][1]]+tot[x];}
inline bool get(int x) {return x==son[fa[x]][1];}
inline void clear(int x) {size[x]=fa[x]=son[x][0]=son[x][1]=tot[x]=val[x]=0;}
inline void rotate(int x)
{int y=fa[x],z=fa[y],k=get(x);son[y][k]=son[x][k^1];fa[son[x][k^1]]=y;son[x][k^1]=y;fa[y]=x;fa[x]=z;if (z) son[z][son[z][1]==y]=x;maintain(x);maintain(y);
}
inline void splay(int x)
{for (R y=fa[x];y;rotate(x),y=fa[x]){if (fa[y]) rotate(get(x)==get(y)?y:x);}root=x;
}
inline void ins(int k)
{if (!root){val[++cnt]=k;++tot[cnt];root=cnt;maintain(root);return;	}int x=root,y=0;while (1){if (val[x]==k){++tot[x];maintain(x);maintain(y);splay(x);break;}y=x;x=son[x][val[x]<k];if (!x){val[++cnt]=k;++tot[cnt];fa[cnt]=y;son[y][val[y]<k]=cnt;maintain(cnt);maintain(y);splay(cnt);break;}}
}
inline int rk(int k)
{int res=0,x=root;while (1){if (k<val[x]) x=son[x][0];else{res+=size[son[x][0]];if (k==val[x]) {splay(x);return res+1;}res+=tot[x];x=son[x][1];}}
}
inline int kth(int k)
{int x=root;while (1){if (son[x][0] && k<=size[son[x][0]]) x=son[x][0];else{k-=size[son[x][0]]+tot[x];if (k<=0) {splay(x);return val[x];}x=son[x][1];}}
}
inline int pre()
{int x=son[root][0];while (son[x][1]) x=son[x][1];splay(x);return x;
}
inline int nxt()
{int x=son[root][1];while (son[x][0]) x=son[x][0];splay(x);return x;
}
inline void del(int k)
{rk(k);if (tot[root]>1) {--tot[root];maintain(root);return;}if (!son[root][0] && !son[root][1]) {clear(root);root=0;return;}if (!son[root][0]) {int x=root;root=son[x][1];fa[root]=0;clear(x);return;}if (!son[root][1]) {int x=root;root=son[x][0];fa[root]=0;clear(x);return;}int x=root,pre1=pre();fa[son[x][1]]=pre1;son[pre1][1]=son[x][1];clear(x);maintain(root);
}
int main()
{read(n);for (R i=1;i<=n;++i){int opt,x;read(opt);read(x);if (opt==1) ins(x);else if (opt==2) del(x);else if (opt==3) printf("%d\n",rk(x));else if (opt==4) printf("%d\n",kth(x));else if (opt==5) ins(x),printf("%d\n",val[pre()]),del(x);else ins(x),printf("%d\n",val[nxt()]),del(x);}return 0;
}

结语

终于写完了累死我了呼这篇博客我写得很用心,相信大家认真看都能初步入门 S p l a y Splay Splay吧,如果有问题可以在评论区提出,博主活着的话都会解答哦,如果有太高级的问题,等到本蒟蒻多进行钻研之后也会回答,白白……


http://chatgpt.dhexx.cn/article/upd3kVO3.shtml

相关文章

【学习笔记】Splay

普通平衡树 模板题链接 1、引入 一种二叉树&#xff0c;这棵树满足任意一个节点&#xff0c;它的左儿子的权值<自己的权值<右儿子的权值 这种树叫做二叉查找树&#xff0c;这个概念应该在初赛中见过了吧 Splay就是利用这个结构来实现的 2、变量 模板题的7大变量 s…

平衡树详解(Splay)

引入 先看例题&#xff1a;&#xff08;洛谷 P3369 【模板】普通平衡树&#xff09; 您需要写一种数据结构&#xff0c;来维护一些数&#xff0c;其中需要提供以下操作&#xff1a; 1.插入 x x x 数 2.删除 x x x 数(若有多个相同的数&#xff0c;因只删除一个) 3.查询 x x…

Splay入门解析【保证让你看不懂(滑稽)】

upd&#xff1a;忽然发现直接在百度上搜索 splay s p l a y 就会搜索到我这篇文章&#xff1f;小蒟蒻瑟瑟发抖。 因为CSDN这边我基本不会看回复什么的&#xff0c;所以如果有问题还是请移步cnblogs里面吧 Cnblogs里面这篇文章的链接 BST真是神奇的东西。。。 而且种类好多呀…

Splay Tree

类别&#xff1a;二叉排序树 空间效率&#xff1a;O(n) 时间效率&#xff1a;O(log n)内完成插入、查找、删除操作 创造者&#xff1a;Daniel Sleator和Robert Tarjan 优点&#xff1a;每次查询会调整树的结构&#xff0c;使被查询频率高的条目更靠近树根。 注&#xff1a;所有…

【算法详解】splay的初步了解

qwq表示最近感受到了不停课的鸭梨啊&#xff0c;好难搞啊……算法太难学了……我好菜啊qwq 其实半个多月前就可以写这篇文章&#xff0c;不过由于时间紧张没写emmmmmm&#xff0c;不扯闲言乱语了&#xff0c;如果有什么写的不好的地方凑活一下吧2333333333 splay是一种可以使树…

splay详解

一&#xff1a;什么是splay&#xff08;伸展树&#xff09; 首先了解二叉排序树 二叉排序树或者是一棵空树&#xff0c;或者是具有下列性质的 二叉树&#xff1a; &#xff08;1&#xff09;若左子树不空&#xff0c;则左子树上所有结点的值均小于或等于它的 根结点的值&#…

Splay 总结基础精华

前言&#xff1a; 第一次学习Splay是2月份&#xff0c;打板子的时候是3月份&#xff0c;Ac是4月份&#xff0c;写这篇博客是6月初&#xff1b;原因是因为我竟然发现我忘了Splay的板子了&#xff01;很慌&#xff0c;必须总结一下&#xff01; 不敢说是最详细的&#xff0c;但希…

关系型数据库与非关系型数据库详解

关系数据库与非关系型数据库 一、数据库概述1、关系型数据库2、非关系型数据库 二、数据库区别1、数据存储方式不同2、扩展方式不同3、对事务性的支持不同 三、非关系型数据库产生背景四、Redis简介1、Redis 优点 五、Redis 安装部署六、Redis 命令工具&#xff08;1&#xff0…

关系型数据库RDS基本简介

OSS存放非结构化的数据&#xff0c;如&#xff1a;音频、视频、图片 rds存放结构化的数据&#xff0c;如&#xff1a;一张表&#xff0c; RDS产品概要 可以根据业务的需求进行弹性伸缩。 采用主从备份架构&#xff0c;三节点数据存储&#xff0c;具备高可用性和数据可靠性 …

2 关系型数据库是什么?

目录结构 关系型数据库基本概念结构化查询语言数据定义语言&#xff08;DDL&#xff09;数据查询语言&#xff08;Data Query Language, DQL&#xff09;数据操纵语言&#xff08;Data Manipulation Language, DML&#xff09;数据控制语言&#xff08;Data Control Language, …

常见的关系型数据库有哪些

Oracle Database&#xff1a;由Oracle公司开发和维护的商业关系型数据库&#xff0c;具有广泛的应用场景和功能。 MySQL&#xff1a;开源关系型数据库&#xff0c;常用于Web应用程序和小型企业应用。 Microsoft SQL Server&#xff1a;由Microsoft公司开发和维护的商业关系型…

Bezier曲线快速相交计算(含代码)

Bezier曲线快速相交计算 背景介绍算法思路解释和分析示例参考资料 背景介绍 很多时候&#xff0c;需要计算曲线段与曲线段是否有交点。常规的思路是直接联立方程求解。不过&#xff0c;直接求方程的解这种思路通常在计算上开销较大。 针对任意曲线&#xff0c;曲线的方程阶次…

js绘制贝塞尔曲线(Bézier-Curve)

贝塞尔曲线(Bzier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线&#xff0c;贝兹曲线由线段与节点组成&#xff0c;节点是可拖动的支点&#xff0c;线段像可伸缩的皮筋&#xff0c;我们…

Bezier曲线与B-Spline曲线

微分几何基础 微分集合是用微分的方法来研究曲线的局部性质&#xff0c;如曲线的弯曲程度等。 一条可以用参数表示的三维曲线是一个有界点集&#xff0c;可以写成一个带参数的、连续的、单值的数学函数&#xff1a; { x x ( t ) y y ( t ) z z ( t ) 0 ⩽ t ⩽ 1 p p ( t…

初识贝塞尔(bezier)曲线

文章目录 资料援引贝塞尔曲线的用途一阶贝塞尔&#xff08;bezier&#xff09;曲线二阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔&#xff08;bezier&#xff09;曲线高阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔曲线求插值&#xff08;Slerp&#xff0…

贝塞尔曲线(Bezier Curve)原理、公式推导及matlab代码实现

1. 定义 贝塞尔曲线(Bezier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线&#xff0c;贝兹曲线由线段与节点组成&#xff0c;节点是可拖动的支点&#xff0c;线段像可伸缩的皮筋&#…

Bezier(贝塞尔曲线通用规律算法-DEMO)

之前也看过一些相关贝塞尔曲线的知识&#xff0c;但就是一直没有实践应用&#xff1b; 今天&#xff0c;听到有同事&#xff1a;程序、美术&#xff0c;在讨论相关的&#xff0c;人物的曲线路径行走的问题&#xff1b; 一些数学比较牛X的&#xff0c;说了用2阶&#xff0c;或…

bezier曲线原理(简单阐述)

原理和简单推导&#xff08;以三阶为例&#xff09;&#xff1a; 设P0、P02、P2是一条抛物线上顺序三个不同的点。过P0和P2点的两切线交于P1点&#xff0c;在P02点的切线交P0P1和P2P1于P01和P11&#xff0c;则如下比例成立&#xff1a; 这是所谓抛物线的三切线定理。 当P0&…

Bezier贝塞尔曲线

1.简介 Bezier曲线在图形学和游戏中经常使用&#xff0c;一般用的比较多的是4个控制点的贝塞尔曲线&#xff0c;这里手写了一个仅供参考&#xff08;注&#xff1a;理论上也可以写任意多个点&#xff08;3个及以上&#xff09;的贝塞尔&#xff0c;就是一个递归的过程&#xff…

java 贝塞尔曲线_在Java中绘制贝塞尔曲线

我需要创建一个简单的Java程序,通过任意数量的点逐个像素地绘制贝塞尔曲线.此刻,一切似乎都没问题,只是曲线总是在x 0 y 0坐标处结束. 截图1 截图2 我需要它在最后一点结束.我的大脑今天工作不太好,所以我正在寻求帮助. 这是我有的&#xff1a; private void drawScene(){ pr…