学习笔记:平衡树-splay
嗯好的今天我们来谈谈cosplay
splay是一种操作,是一种调整二叉排序树的操作,但是它并不会时时刻刻保持一个平衡,因为它会根据每一次操作把需要操作的点旋转到根节点上
所谓二叉排序树,就是满足对树中的任意一个节点,它左子树上的任意一个值比它的值小,右子树上的任意一个值比它的值大的一棵二叉树 ;至于平衡:是一棵空树或任意节点的左右两个子树的深度差的绝对值不超过1(from:百度百科) 看图:
不平衡: 平衡:
可以观察到这两棵树都是满足⑥<⑤<④<③<①<②的大小关系,只是改变每个点的相对位置不同。
然后呢,splay的真正含义就出来了:通过很多很多次变换把要处理的点放到根节点上。与此同时,我们把这种变换叫做旋转。旋转的精髓就是在不破坏树中的大小顺序的同时改变节点的位置。旋转是splay的核心(虽然大部分排序树的核心操作都是旋转),大概思路和treap是差不多的(没学过treap然后爆发出狂笑),但对于splay相对来说懒一点的是它转的时候是自动判断左旋还是右旋。每一次旋转,我们把被旋转的这个点向上转一层(也就是转到它父节点的位置上)
比如说要旋转⑤节点: 可以看看旋转之后的样子:
因为⑤<④<③,所以要把⑤旋转后应该是③的左子树(也就是④的位置),但是⑧始终是大于④的所以⑧的位置是不变的 (④的右子树),然后因为⑤>④>⑦,所以④和⑦都应该是⑤的右子树而且⑦应该是④的左子树(因为是一棵二叉树),然后可以发现与⑥,⑧没有任何关系,真正改变的就只有图中的红线(表示各个节点间的父子关系)。这就是旋转的步骤。
但如果⑤是④的右子树呢?其实是差不多的,就是左右翻转一下。再想想这个图,自己推一遍:
好的,那么旋转的普遍规律就可以看出来了:整个旋转的操作中改变的只是被旋转点、它的父节点、以及它的某一棵子树,到底是哪一棵子树是根据被旋转点与它父亲的关系来决定的:如果这个点是它父节点的左子树,那么应该调整这个点的右子树;如果是右子树,则相反。
(有两个很棒的动图来表示旋转)
接下来打出代码。在那之前,我们先定义数组:
son[i][0]是指点i的左节点编号,son[i][1]是右节点编号
root表示当前根节点的编号
sz是指整个树的值的种数,同时用于新节点插入时的编号(可以类比时间戳)
siz[i]表示以i为顶点的树的值的个数,要计算重复出现的值(包括i节点自己)
key[i]表示节点i的值是多少
fa[i]表示节点i的父节点编号
cnt[i]表示节点i的值出现了多少次(数量cnt,siz[i]和种类sz是两个东西不要搞混了)
因为旋转的方式是由它是左子树还是右子树决定的,所以我们可以先写一个函数来判断:
int get(int x){
return son[fa[x]][]==x;//如果它父节点的右儿子编号等于它那么返回1(右节点),否则返回0(是左节点)
}
接下来就是旋转了:
void rotate(int x){
int f=fa[x],ff=fa[f],w=get(x);//父节点、祖父节点、是父节点的左子树还是右子树
son[f][w]=son[x][w^];//x节点的另一个子树放给原本x节点的位置
fa[son[f][w]]=f;//更新x节点的另一个子树的父节点
son[x][w^]=f;//将父节点接到x节点的另一个子树上
fa[f]=x;
fa[x]=ff;//f、x位置互换后更新祖父节点
if(ff){//父节点不是根节点(根节点的父节点为0)
son[ff][son[ff][]==f]=x;
}
update(f);
update(x);
}
注意这里的^位运算,意思是两位不同时返回1,相同时返回0,这里^1可以快速找出另一个儿子(0^1=1,1^1=0)
这里的update是指旋转之后由于位置的改变而引起的种类总数的变化,因为每一个节点值的出现的次数是没有改变的,其实很简单就是左子树种类加上右子树种类:
void update(int x){
if(x!=){//如果是根节点,旋转时种类数始终不变
siz[x]=cnt[x];//自身值的出现次数
if(son[x][])//如果有左子树
siz[x]+=siz[son[x][]];
if(son[x][])//如果有右子树
siz[x]+=siz[son[x][]];
}
}
这只是一次操作,我们前面说了我们是把要处理的点旋转到根节点上,那么怎么做呢?循环就行了。但是还有一种特殊情况,由于这种情况都满足被旋转点和父节点都是左节点或者是右儿子,我们姑且称它为三点一线,先看图:
比如说我们这里要splay④,如果直接把④一直旋转到根节点的话就会是这样:
可以看见③还是①的左节点,相当于只是改变了④和①的关系,专业一点就是说形成了单旋使平衡树失衡。而解决的方法就是在出现三点一线时先旋转它的父节点避免单旋,正确的应该是这样:
void splay(int x){
for(int f;f=fa[x];rotate(x)){//注意旋转的始终是x
if(fa[f]){//可能存在三点一线
rotate(get(x)==get(f)?f:x);//三点一线情况判断
}
}
}
接下来是splay的插入:由于在旋转的时候,我们是建立在这棵树是有序的前提下的,而要保证这个前提,就需要从这棵树的建立开始就让它有序,所以,我们在插入的时候就必须按照排序树的规定来插入,也就是说每次插入都是从根节点开始比较大小,直到找到这个值或者是找到树的底部(这个值没有出现过):
void insect(int v){
if(sz==){//如果这棵树是空树,插入点应该为根节点
sz++;
son[][]=son[][]=fa[]=;
siz[]=cnt[]=;
root=;
key[]=v;
return;
}
int now=root,f=;//now表示现在查找到节点编号,f表示当前节点的父节点
while(){
if(key[now]==v){//如果这个值已经在树中,那么它出现的次数增加
cnt[now]++;
update(now);//更新相关点
update(f);
splay(now);//将插入的点旋转到根节点,便于下一次可能的操作
break;
}
f=now;
now=son[now][v>key[now]];//依照节点值查找位置,如果大于当前值v>key[now]=1,则在右子树范围内,反之亦然
if(now==){//树中无这个值,查找到树的底端,新建一个子节点
sz++;//新节点编号
son[sz][]=son[sz][]=;//新节点初始化
fa[sz]=f;
siz[sz]=cnt[sz]=;
son[f][v>key[now]]=sz;//判断新节点是左节点还是右节点并更新父节点
key[sz]=v;
update(f);//更新数量
splay(sz);//旋转到根节点
break;
}
}
}
接下来是查找一个值在树中排从小到大第几(是比它小的有多少个)(如果为了方便理解也可以说是从大到小求倒数第几),原理很简单,因为排序树的性质,我们可以知道只要是在这个节点的左边的都是比它小的,那么我们就可以利用之前记录好的siz来快速求出:(注意是数量,要加上重复的,比如说1,2,2,3,这个数列中,3排第4)
int find(int v){
int ans=,now=root;//ans记录已经有多少比它小的点,now表示正在寻找的节点的编号
while(){
if(v<key[now]){//如果当前节点的值大于v,那么当前节点的左子树不完全小于v,继续向当前节点的左子树寻找
now=son[now][];
}
else{//当前节点的左子树上的值必然全部小于v
ans+=(son[now][]!=?siz[son[now][]]:);//如果有左子树则直接加上左子树的数量
if(v==key[now]){//如果当前节点的值等于v,则右子树上不可能有比它小的数,所有比它小的数已经找完
splay(now);//下一次可能的操作
return ans+;//有1个数比它小那么它应该是第2,以此类推,要+1
}
ans+=cnt[now];//key[now]<v的情况,除了它的左儿子还要加上它自身的数量
now=son[now][];//右子树中可能存在比v小的值,所以在右子树中继续寻找
}
}
}
有了查找排名,如果需要知道第几是多少,也是可以求的:
int findx(int x){
int now=root;//当前节点
while(){
if(son[now][]!=&&siz[son[now][]]>=x){//如果左子树的数量大于x,就是说第x个是在左子树上(前提是有左子树)
now=son[now][];//在左子树上接着搜索
}
else{//第x个在以当前节点为顶点的树中
int less=(son[now][]!=?siz[son[now][]]:)+cnt[now];//左子树的数量(可能没有)+当前节点的值的数量
if(x<=less)//由于之前判断过是否在左子树上,并且在之后的运算中排除了所有左子树,x却不在右子树上,那么只可能是当前点的值
return key[now];
x-=less;//在右子树中还有多少值比它小,排除左子树
now=son[now][];//继续搜索
}
}
}
接下来是前驱,就是指比它小的第一个点(就是比它自己的值小而且它的前驱与它自己的值的差的绝对值最小)。在树中表现为它的左子树中最右边的那个点,这样才满足比它自身的值小并且最接近它自身的值:
int query_pre(int x){
splay(x);//首先旋转到根节点方便查找
int now=son[root][];//定位左子树
while(son[now][]!=)now=son[now][];//循环查找左子树中最右边的点
return now;//最底部跳出循环,找到答案
}
有前驱当然也有后继,类似的,后继是指比它大的第一个点(比它自己的值大而且它的后继与它自己的值的差的绝对值最小)。在树中表现为它右子树最左边的那个点:(原理一模一样)
int query_next(int x){
splay(x);
int now=son[root][];
while(son[now][]!=)now=son[now][];
return now;
}
Last but not the least:删除。这里的删除是删除一次值,就是说受cnt的影响,不一定是删一个节点。这个挺复杂的,比如说删除一个值v,首先我们用之前的find(v)将值等于v的点旋转到根上,之后要分5种情况:
- 这个值出现了不止一次:直接减少次数,更新数量即可;
- 整棵树只剩下它一个值 孤苦伶仃 :将树清空;
- 这个点没有左子树:直接将右子树的顶点提出来,取代它的父节点;
- 这个点没有右子树:直接将左子树的顶点提出来,取代它的父节点;
- 这个点左右子树都有:先把前驱为作新树的根(叫做新根)(后继也完全没有问题,只是之后的处理要想一想是左子树还是右子树),将它旋转到根,这个时候在将原根的右儿子接到新根的左儿子上,更新即可。
这里先给出清空:(就是把所有关于这个点的东西归零)
void clear(int x){
son[x][]=son[x][]=fa[x]=siz[x]=key[MX]=cnt[x]=;
}
接下来就可以开始删除了:
void del(int v){
find(v);
if(cnt[root]>){//第一种情况
cnt[root]--;
update(root);
return;
}
if(son[root][]==&&son[root][]==){//第二种情况
clear(root);
root=;//将树清空
return;
}
if(son[root][]==){//第三种情况
int old=root;
root=son[root][];
fa[root]=;//新根的父节点更新
clear(old);
return;
}
if(son[root][]==){//第四种情况
int old=root;
root=son[root][];
fa[root]=;
clear(old);
return;
}
int newroot=query_per(root),oldroot=root;
splay(newroot);//将新根转上来
fa[son[oldroot][]]=newroot;
son[root][]=son[oldroot][];//继承右子树
clear(oldroot);//旧根归零
update(root);//更新新根
}
学习笔记:平衡树-splay的更多相关文章
- 学习笔记--(平衡树)splay
坑爹的splay,毁我青春,耗我钱财,颓我精力 是一种用于保存有序集合的简单高效的数据结构.伸展树实质上是一个二叉查找树.允许查找,插入,删除,删除最小,删除最大,分割,合并等许多操作,这些操作的时间 ...
- [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家
1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...
- 【学习笔记】splay入门(更新中)
声明:本博客所有随笔都参照了网络资料或其他博客,仅为博主想加深理解而写,如有疑问欢迎与博主讨论✧。٩(ˊᗜˋ)و✧*。 前言 终于学习了 spaly \(splay\) !听说了很久,因为dalao总 ...
- 普通平衡树学习笔记之Splay算法
前言 今天不容易有一天的自由学习时间,当然要用来"学习".在此记录一下今天学到的最基础的平衡树. 定义 平衡树是二叉搜索树和堆合并构成的数据结构,它是一 棵空树或它的左右两个子树的 ...
- [学习笔记] 平衡树——Treap
前置技能:平衡树前传:BST 终于学到我们喜闻乐见的平衡树啦! 所以我们这次讲的是平衡树中比较好写的\(Treap\). (以后会写splay的先埋个坑在这) 好了,进入正题. step 1 我们知道 ...
- 学习笔记:Splay
代码适中.非常灵活的平衡树. 需要前置:二叉搜索树. 一些基础的函数: int idx, ch[N][2], cnt[N], sz[N], fa[N]; /* idx 是节点计数, ch[i][0 / ...
- 学习笔记 | treap | splay
目录 前言 treap 它的基本操作 前言 不会数据结构选手深深地感受到了来自treap的恶意QwQ 在听的时候感觉自己听得听懂的??大概只是听懂了它的意思 代码是怎么写都感觉写不好╮(╯﹏╰)╭ 菜 ...
- 平衡树学习笔记(3)-------Splay
Splay 上一篇:平衡树学习笔记(2)-------Treap Splay是一个实用而且灵活性很强的平衡树 效率上也比较客观,但是一定要一次性写对 debug可能不是那么容易 Splay作为平衡树, ...
- BST,Splay平衡树学习笔记
BST,Splay平衡树学习笔记 1.二叉查找树BST BST是一种二叉树形结构,其特点就在于:每一个非叶子结点的值都大于他的左子树中的任意一个值,并都小于他的右子树中的任意一个值. 2.BST的用处 ...
- 平衡树splay学习笔记#2
讲一下另外的所有操作(指的是普通平衡树中的其他操作) 前一篇的学习笔记连接:[传送门],结尾会带上完整的代码. 操作1,pushup操作 之前学习过线段树,都知道子节点的信息需要更新到父亲节点上. 因 ...
随机推荐
- 推荐一个小而美的Python代码格式化工具
代码可读性是评判代码质量的标准之一,有一个衡量代码质量的标准是 Martin 提出的 “WFT” 定律,即每分钟爆出 “WTF” 的次数.你在读别人代码或者做 Code Review 的时候有没有 “ ...
- 计算某天的下一天:黑盒测试之等价类划分+JUnit参数化测试
题目要求 测试以下程序:该程序有三个输入变量month.day.year(month.day和year均为整数值,并且满足:1≤month≤12.1≤day≤31和1900≤year≤2050),分别 ...
- phpcms模块安装
工作中需要用到 phpcms开源框架,借鉴了 http://www.cnblogs.com/benpaodelulu/p/6874201.html这个地址,搞定的 ,非常实用 如果有用到的朋友们可 ...
- Excel导入异常Cannot get a text value from a numeric cell解决及poi导入时注意事项
POI操作Excel时偶尔会出现Cannot get a text value from a numeric cell的异常错误. 异常原因:Excel数据Cell有不同的类型,当我们试图从一个数字类 ...
- 【Linux常见问题】CentOS 7 root用户密码忘记,找回密码方法
1.开机按esc 2.选择CentOS Linux (3.10.0-693.......) 按 e 键: 3.光标移动到 linux 16 开头的行,找到 ro 改为 rw init=sysr ...
- MongoDB学习(四):通过Java使用MongoDB
环境配置 在Java项目中使用MongoDB,需要在项目中引入mongo.jar这个包.下载地址:下载 请尽量下载较新的版本,本文用的是2.10.1. 连接MongoDB public synchro ...
- 解决 Retrofit 多 BaseUrl 及运行时动态改变 BaseUrl ?
原文地址: juejin.im/post/597856- 解决Retrofit多BaseUrl及运行时动态改变BaseUrl(一) 解决Retrofit多BaseUrl及运行时动态改变BaseUrl( ...
- python练习---博客登录(装饰器)
程序要求: 1),启动程序,首页面应该显示成如下格式: 欢迎来到博客园首页 1:请登录 2:请注册 3:文章页面 4:日记页面 5:评论页面 6:收藏页面 7:注销 8:退出程序2),用户输入选项,3 ...
- 使用PHP-Beast加密你的PHP源代码
PHP-Beast是一个PHP源码加密的模块,其使用DES算法加密,用户可以自定义加密的key来加密源代码. 1. PHP-Beast的安装 $ wget https://github.com/lie ...
- UDP广播的客户端和服务器端的代码设计
实验环境 linux 注意: 使用UDP广播,是客户端发送广播消息,服务器端接收消息.实际上是客户端探测局域网中可用服务器的一种手段.客户端发送,服务器端接收,千万不能弄混淆!!! 为了避免混淆,本文 ...