学习笔记:平衡树-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]][1]==x;//如果它父节点的右儿子编号等于它那么返回1(右节点),否则返回0(是左节点) }
接下来就是旋转了:
void rotate(int x){int f=fa[x],ff=fa[f],w=get(x);//父节点、祖父节点、是父节点的左子树还是右子树son[f][w]=son[x][w^1];//x节点的另一个子树放给原本x节点的位置fa[son[f][w]]=f;//更新x节点的另一个子树的父节点son[x][w^1]=f;//将父节点接到x节点的另一个子树上fa[f]=x;fa[x]=ff;//f、x位置互换后更新祖父节点if(ff){//父节点不是根节点(根节点的父节点为0)son[ff][son[ff][1]==f]=x; }update(f);update(x); }
注意这里的^位运算,意思是两位不同时返回1,相同时返回0,这里^1可以快速找出另一个儿子(0^1=1,1^1=0)
这里的update是指旋转之后由于位置的改变而引起的种类总数的变化,因为每一个节点值的出现的次数是没有改变的,其实很简单就是左子树种类加上右子树种类:
void update(int x){if(x!=0){//如果是根节点,旋转时种类数始终不变siz[x]=cnt[x];//自身值的出现次数if(son[x][0])//如果有左子树siz[x]+=siz[son[x][0]];if(son[x][1])//如果有右子树siz[x]+=siz[son[x][1]];} }
这只是一次操作,我们前面说了我们是把要处理的点旋转到根节点上,那么怎么做呢?循环就行了。但是还有一种特殊情况,由于这种情况都满足被旋转点和父节点都是左节点或者是右儿子,我们姑且称它为三点一线,先看图:
比如说我们这里要splay④,如果直接把④一直旋转到根节点的话就会是这样:
可以看见③还是①的左节点,相当于只是改变了④和①的关系,专业一点就是说形成了单旋使平衡树失衡。而解决的方法就是在出现三点一线时先旋转它的父节点避免单旋,正确的应该是这样:
void splay(int x){for(int f;f=fa[x];rotate(x)){//注意旋转的始终是xif(fa[f]){//可能存在三点一线rotate(get(x)==get(f)?f:x);//三点一线情况判断}} }
接下来是splay的插入:由于在旋转的时候,我们是建立在这棵树是有序的前提下的,而要保证这个前提,就需要从这棵树的建立开始就让它有序,所以,我们在插入的时候就必须按照排序树的规定来插入,也就是说每次插入都是从根节点开始比较大小,直到找到这个值或者是找到树的底部(这个值没有出现过):
void insect(int v){if(sz==0){//如果这棵树是空树,插入点应该为根节点sz++;son[1][1]=son[1][0]=fa[1]=0;siz[1]=cnt[1]=1;root=1;key[1]=v;return;}int now=root,f=0;//now表示现在查找到节点编号,f表示当前节点的父节点while(1){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==0){//树中无这个值,查找到树的底端,新建一个子节点sz++;//新节点编号son[sz][1]=son[sz][0]=0;//新节点初始化fa[sz]=f;siz[sz]=cnt[sz]=1;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=0,now=root;//ans记录已经有多少比它小的点,now表示正在寻找的节点的编号while(1){if(v<key[now]){//如果当前节点的值大于v,那么当前节点的左子树不完全小于v,继续向当前节点的左子树寻找now=son[now][0];}else{//当前节点的左子树上的值必然全部小于vans+=(son[now][0]!=0?siz[son[now][0]]:0);//如果有左子树则直接加上左子树的数量if(v==key[now]){//如果当前节点的值等于v,则右子树上不可能有比它小的数,所有比它小的数已经找完splay(now);//下一次可能的操作return ans+1;//有1个数比它小那么它应该是第2,以此类推,要+1}ans+=cnt[now];//key[now]<v的情况,除了它的左儿子还要加上它自身的数量now=son[now][1];//右子树中可能存在比v小的值,所以在右子树中继续寻找}} }
有了查找排名,如果需要知道第几是多少,也是可以求的:
int findx(int x){int now=root;//当前节点while(1){if(son[now][0]!=0&&siz[son[now][0]]>=x){//如果左子树的数量大于x,就是说第x个是在左子树上(前提是有左子树)now=son[now][0];//在左子树上接着搜索}else{//第x个在以当前节点为顶点的树中int less=(son[now][0]!=0?siz[son[now][0]]:0)+cnt[now];//左子树的数量(可能没有)+当前节点的值的数量if(x<=less)//由于之前判断过是否在左子树上,并且在之后的运算中排除了所有左子树,x却不在右子树上,那么只可能是当前点的值return key[now];x-=less;//在右子树中还有多少值比它小,排除左子树now=son[now][1];//继续搜索}} }
接下来是前驱,就是指比它小的第一个点(就是比它自己的值小而且它的前驱与它自己的值的差的绝对值最小)。在树中表现为它的左子树中最右边的那个点,这样才满足比它自身的值小并且最接近它自身的值:
int query_pre(int x){ splay(x);//首先旋转到根节点方便查找int now=son[root][0];//定位左子树while(son[now][1]!=0)now=son[now][1];//循环查找左子树中最右边的点return now;//最底部跳出循环,找到答案 }
有前驱当然也有后继,类似的,后继是指比它大的第一个点(比它自己的值大而且它的后继与它自己的值的差的绝对值最小)。在树中表现为它右子树最左边的那个点:(原理一模一样)
int query_next(int x){splay(x);int now=son[root][1];while(son[now][0]!=0)now=son[now][0];return now; }
Last but not the least:删除。这里的删除是删除一次值,就是说受cnt的影响,不一定是删一个节点。这个挺复杂的,比如说删除一个值v,首先我们用之前的find(v)将值等于v的点旋转到根上,之后要分5种情况:
- 这个值出现了不止一次:直接减少次数,更新数量即可;
- 整棵树只剩下它一个值 孤苦伶仃 :将树清空;
- 这个点没有左子树:直接将右子树的顶点提出来,取代它的父节点;
- 这个点没有右子树:直接将左子树的顶点提出来,取代它的父节点;
- 这个点左右子树都有:先把前驱为作新树的根(叫做新根)(后继也完全没有问题,只是之后的处理要想一想是左子树还是右子树),将它旋转到根,这个时候在将原根的右儿子接到新根的左儿子上,更新即可。
这里先给出清空:(就是把所有关于这个点的东西归零)
void clear(int x){son[x][0]=son[x][1]=fa[x]=siz[x]=key[MX]=cnt[x]=0; }
接下来就可以开始删除了:
void del(int v){find(v);if(cnt[root]>1){//第一种情况cnt[root]--;update(root);return;}if(son[root][0]==0&&son[root][1]==0){//第二种情况clear(root);root=0;//将树清空return;}if(son[root][0]==0){//第三种情况int old=root;root=son[root][1];fa[root]=0;//新根的父节点更新clear(old);return;}if(son[root][1]==0){//第四种情况int old=root;root=son[root][0];fa[root]=0;clear(old);return;}int newroot=query_per(root),oldroot=root;splay(newroot);//将新根转上来fa[son[oldroot][1]]=newroot;son[root][1]=son[oldroot][1];//继承右子树clear(oldroot);//旧根归零update(root);//更新新根 }
转载于:https://www.cnblogs.com/lazy-people/p/9326556.html
学习笔记:平衡树-splay相关推荐
- 【学习笔记】Splay
普通平衡树 模板题链接 1.引入 一种二叉树,这棵树满足任意一个节点,它的左儿子的权值<自己的权值<右儿子的权值 这种树叫做二叉查找树,这个概念应该在初赛中见过了吧 Splay就是利用这个 ...
- 平衡树(splay)学习笔记(详细,从入门到精(bao)通(ling))(持续更新)
前言 在前几天军训站军姿的时候胡思乱想,突然明白了splay的本质 KMP学习笔记后又一篇字数上万的题解- 前置技能--二叉搜索树 首先来看一个最简单的问题: 你需要维护一个数据结构,资磁这些操作: ...
- 平衡树 - FHQ 学习笔记
平衡树 - FHQ 学习笔记 主要参考万万没想到 的 FHQ-Treap学习笔记. 本片文章的姊妹篇:平衡树 - Splay 学习笔记. 感觉完全不会平衡树,又重新学习了一遍 FHQ,一口气把常见套路 ...
- pbds库学习笔记(优先队列、平衡树、哈希表)
目录 pbds库学习笔记(优先队列.平衡树.哈希表) 前言 概述 priority_queue优先队列 概述 参数 堆的基本操作的函数 对比STL新增函数 modify修改 Dijkstra最短路径演 ...
- 平衡树学习笔记之 fhq Treap
平衡树学习笔记 1:fhq Treap(非旋 Treap) 正文开始前首先 %%% fhq 大佬. 众所周知,平衡树是一种 非常猥琐 码量堪忧的数据结构. 他的祖先是一种叫做二叉搜索树 ( B S T ...
- Treap平衡树学习笔记
Treap平衡树学习笔记 放在前面的话 与信息学竞赛告别9个月后,中考终于结束,我终于复出了... 结果一回来就学数据结构,学得我有点懵... 本文写了两个多星期... 尽管学习Treap感到万分痛苦 ...
- [学习笔记]可持久化数据结构——数组、并查集、平衡树、Trie树
可持久化:支持查询历史版本和在历史版本上修改 可持久化数组 主席树做即可. [模板]可持久化数组(可持久化线段树/平衡树) 可持久化并查集 可持久化并查集 主席树做即可. 要按秩合并.(路径压缩每次建 ...
- 【学习笔记】线段树详解(全)
[学习笔记]线段树详解(全) 和三个同学一起搞了接近两个月的线段树,头都要炸了T_T,趁心态尚未凉之前赶快把东西记下来... [目录] [基础]作者:\((Silent\)_\(EAG)\) [懒标记 ...
- MySQL服务器学习笔记!(二) ——数据库各项操作
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://foreveryan.blog.51cto.com/3508502/657640 ...
- Link Cut Tree 学习笔记
Link Cut Tree 学习笔记 说在前边 最近补 CF 碰见一道 LCT ,就打算学习一下这个东西...顺便复习一下 splay. 具体算法及实现 参考了FlashHu, Candy? P369 ...
最新文章
- Python Web 框架要点
- liunx导出mysql慢查询日志查看_查看 MySQL 慢查询日志文件-问答-阿里云开发者社区-阿里云...
- web应用如何确定能同时允许多少用户连接?_Web测试环境搭建+测试要点汇总
- Select下拉列表框(添加、删除option)
- linux系统找回数据,Linux系统上面误删的数据找回
- 电影点评系统论文java_毕业设计(论文)-基于web的电影点评系统分析与设计.docx...
- 虚拟机上怎么配置mysql数据库_Linux虚拟机下安装配置MySQL
- 关于框架的胡言乱语(上)
- fread函数和fwrite函数,read,write
- 你增长的年龄,是因为丢掉了快乐吗?
- 浏览器存储,储存,Cookie,WebStorage,IndexedDB
- WordPress搬家插件迁移网站的方法(从一台服务器搬到另一台服务器)
- 二叉搜索树的后序遍历序列验证
- ShadowGun 体积光学习
- 进程之间究竟有哪些通信方式
- Docker离线安装部署文档
- 洛谷 P2672 推销员
- 如何找回电脑回收站删除的文件, 10种恢复工具方法!
- ati linux驱动下载,安装 ATI 驱动 for linux
- AC695X_独立3路串口UART收发数据配置
热门文章
- 1月4日云栖精选夜读:阿里工程师如何叫外卖?99%的人猜不到
- onSaveInstanceState() 和 onRestoreInstanceState()
- 网络聊天室的技术与发展
- scp在Linux主机之间copy不用输入密码
- 爱普生690k打印针测试软件_办公室打印机什么牌子好 办公室打印机怎么选购【详解】...
- HTML之图片标签、音视频标签
- Redis内存回收和持久化策略
- linux运行win7,Windows7 上运行docker实战
- Java虚拟机(四)——运行时数据区
- 安卓中java或取上下文_android-Xposed:如何获取挂钩的应用程序的上下文?