平衡树splay学习笔记#2
讲一下另外的所有操作(指的是普通平衡树中的其他操作)
前一篇的学习笔记连接:【传送门】,结尾会带上完整的代码。
操作1,pushup操作
之前学习过线段树,都知道子节点的信息需要更新到父亲节点上。
因为旋转之后有两个节点的儿子和两个节点的父亲被改变了,那么原来的总儿子个数也就是sz就被改变了。
那么我们需要维护sz,就需要pushup操作。
这个东西比较简单。
void pushup(int nod) {
tr[nod].sz = tr[tr[nod].ch[0]].sz + tr[tr[nod].ch[1]].sz + tr[nod].cnt;
}
操作2,插入操作(insert)
上一次的博客已经讲过了,如果有相同权值的节点,那么就放到一个相同的节点内,将cnt个数+1,如果没有那么就新开一个。
insert操作就是这样实现的。
请看如下的图片:
我们要讲这个权值为8的节点插入到BST中,根据BST的原则,不能有重复的节点,但是在这里不存在。
很明显这个8一定是变成了权值为9的节点的左儿子,因为他比5要大,但是比9要小。
那么我们就可以像二分查找一样的思路,找到9的位置。如果当前的节点权值比插入的权值大,那么就到节点的左节点查找,反之到右节点,这也是后面查找的原理。
上图粉色的线就是我们查找的路径。
插入节点后我们会发现,那条链又出来了,我们就开始splay操作,将这个u转到根节点的位置,来改变树的形状。
void ins(int x) {
int u = rt, ft = 0;//u表示当前的节点,ft表示u的父亲,因为我们需要更新父亲的子节点。
while (u && tr[u].val != x) {//如果u不是空的节点,而且没有找到相同权值的节点,那么就继续向下查找
ft = u;//记录父亲
u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
}
if (u) tr[u].cnt ++;//如果有相同的权值的节点,那么就在cnt上标记2+1
else {
u = ++ tot;//就新加一个节点
if (ft) tr[ft].ch[x > tr[ft].val] = u;//如果这个节点是根节点,那么就没有必要更新父亲节地了。
tr[u].init(x, ft);//插入节点的所有信息
}
splay(u, 0);//将节点
}
操作3,查询操作,find
上面稍微提到了一点点,类似于二分查找,不过只是在树上,而且已经满足了BST的性质了。
分两种情况讨论
- 如果当前节点的权值>查询的值,那么说明这个节点可能在左子树中,那么查询左子树
- 如果当前节点的权值<查询的值,那么说明这个节点可能在右子树中,那么查询右子树
为了方便我们接下来的操作和直接调用查询,那么我们就选择了把这个节点旋转根节点的位置。
因为和二分查找差不多,所以时间复杂度差不多就是logn。
void find(int x) {
int u = rt;
if (!u) return;//如果是空的,那么就跳过。
while (tr[u].ch[x > tr[u].val] && x != tr[u].val) {//如果没有找到或者是没有走到底部
u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
}
splay(u, 0);//旋转到根节点
}
操作4,查找前驱和后继
理解了find操作后应该也能理解前驱和后继。
所谓前驱就是小于它的最大的数,后继就是大于它的最小的数。
那么严格前驱和严格后继就是不存在相同的情况。
首先是前驱,前驱是小于它的数,那么一定是在当前节点的左子树中。
这个时候我们的旋转到根节点的操作就派上用场了,因为根节点就是我们需要查找的节点,所以可以直接操作。
然后以为是最大的数,那么也就是在左子树中一直往右儿子边走。
那么同理后继就是在右子树中一种往左儿子走。是不是特别好理解。
如果还是不理解的话,我下次考虑画一个图,这样更加清晰。
yyb巨佬喜欢吧前驱和和后继写到一起,但是我更喜欢分开来写,虽然代码长了一点,但是更加清楚
int pre(int x) {//前驱
find(x);
int u = rt;
if (tr[u].val < x) return u;//如果根节点的答案就是小于的,那么就直接输出。
u = tr[u].ch[0];//到左子树中
while (tr[u].ch[1]) u = tr[u].ch[1];//一直往右儿子边走
return u;
}
int suc(int x) {
find(x);
int u = rt;
if (tr[u].val > x) return u;//如果根节点的答案就是大于的,那么就直接输出。
u = tr[u].ch[1];//到右子树中
while (tr[u].ch[0]) u = tr[u].ch[0];//一直往左儿子走
return u;
}
操作5,删除操作(delete)
现在就很简单啦
首先找到这个数的前驱,把他Splay到根节点
然后找到这个数后继,把他旋转到前驱的底下
比前驱大的数是后继,在右子树
比后继小的且比前驱大的有且仅有当前数
在后继的左子树上面,
因此直接把当前根节点的右儿子的左儿子删掉就可以啦
void del(int x) {
int lst = pre(x), nxt = suc(x);//查找前驱和后继
splay(lst, 0); //将lst转到根节点
splay(nxt, lst);//将nxt转到lst的位置。
int del = tr[nxt].ch[0];//那么删除的数就是nxt的左儿子
if (tr[del].cnt > 1) {//如果原来就>1个,那么直接-1
tr[del].cnt --;
splay(del, 0);
}
else tr[nxt].ch[0] = 0;//不然直接删除
}
操作6,查找第k大(kth)
这个也非常简单。
如果当前节点的左子树+节点cnt还是小于k,那么说明在右子树
k-=上面所说的和,查找右子树
如果左子树的节点个数足够,那么就查找左子树
否则一定是在当前的根节点,直接输出根节点。
int kth(int x) {
int u = rt;
if (tr[u].sz < x) return 0;//如果不存在x个,那么就输出0
while (1) {
int lc = tr[u].ch[0];//左儿子
if (x > tr[lc].sz + tr[u].cnt) {//情况1
x -= tr[lc].sz + tr[u].cnt;
u = tr[u].ch[1];//跳右儿子
}
else {
if (tr[lc].sz >= x) u = lc;//如果左儿子节点的个数足够,那么就到左儿子上面查找
else return tr[u].val;//不然就是在当前的节点上
}
}
}
结尾AC完整代码
#include <bits/stdc++.h>
#define N 100005
#define inf 2147483647
using namespace std;
template <typename T>
inline void read(T &x) {
x = 0; T fl = 1;
char ch = 0;
while (ch < '0' || ch > '9') {
if (ch == '-') fl = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
x *= fl;
}
struct Splay {
int rt, tot;
struct node {
int val, fa, cnt, sz, ch[2];
void init(int x, int ft) {
fa = ft;
val = x;
ch[1] = ch[0] = 0;
sz = cnt = 1;
}
}tr[N];
Splay() {
memset(tr, 0, sizeof(tr));
rt = tot = 0;
}
void pushup(int nod) {
tr[nod].sz = tr[tr[nod].ch[0]].sz + tr[tr[nod].ch[1]].sz + tr[nod].cnt;
}
void rotate(int nod) {
int fa = tr[nod].fa, gf = tr[fa].fa, k = tr[fa].ch[1] == nod;
tr[gf].ch[tr[gf].ch[1] == fa] = nod;
tr[nod].fa = gf;
tr[fa].ch[k] = tr[nod].ch[k ^ 1];
tr[tr[nod].ch[k ^ 1]].fa = fa;
tr[nod].ch[k ^ 1] = fa;
tr[fa].fa = nod;
pushup(fa);
pushup(nod);
}
void splay(int nod, int goal) {
while (tr[nod].fa != goal) {
int fa = tr[nod].fa, gf = tr[fa].fa;
if (gf != goal) {
if ((tr[gf].ch[0] == fa) ^ (tr[fa].ch[0] == nod)) rotate(nod);
else rotate(fa);
}
rotate(nod);
}
if (goal == 0) rt = nod;
}
void find(int x) {
int u = rt;
if (!u) return;//如果是空的,那么就跳过。
while (tr[u].ch[x > tr[u].val] && x != tr[u].val) {//如果没有找到或者是没有走到底部
u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
}
splay(u, 0);//旋转到根节点
}
void ins(int x) {
int u = rt, ft = 0;//u表示当前的节点,ft表示u的父亲,因为我们需要更新父亲的子节点。
while (u && tr[u].val != x) {//如果u不是空的节点,而且没有找到相同权值的节点,那么就继续向下查找
ft = u;//记录父亲
u = tr[u].ch[x > tr[u].val];//x>tr[u].val时,那么就是1也就是跳到右儿子,否则是跳到左儿子。
}
if (u) tr[u].cnt ++;//如果有相同的权值的节点,那么就在cnt上标记2+1
else {
u = ++ tot;//就新加一个节点
if (ft) tr[ft].ch[x > tr[ft].val] = u;//如果这个节点是根节点,那么就没有必要更新父亲节地了。
tr[u].init(x, ft);//插入节点的所有信息
}
splay(u, 0);//将节点
}
int pre(int x) {
find(x);
int u = rt;
if (tr[u].val < x) return u;//如果根节点的答案就是小于的,那么就直接输出。
u = tr[u].ch[0];//到左子树中
while (tr[u].ch[1]) u = tr[u].ch[1];//一直往右儿子边走
return u;
}
int suc(int x) {
find(x);
int u = rt;
if (tr[u].val > x) return u;//如果根节点的答案就是大于的,那么就直接输出。
u = tr[u].ch[1];//到右子树中
while (tr[u].ch[0]) u = tr[u].ch[0];//一直往左儿子走
return u;
}
void del(int x) {
int lst = pre(x), nxt = suc(x);//查找前驱和后继
splay(lst, 0); //将lst转到根节点
splay(nxt, lst);//将nxt转到lst的位置。
int del = tr[nxt].ch[0];//那么删除的数就是nxt的左儿子
if (tr[del].cnt > 1) {//如果原来就>1个,那么直接-1
tr[del].cnt --;
splay(del, 0);
}
else tr[nxt].ch[0] = 0;//不然直接删除
}
int kth(int x) {
int u = rt;
if (tr[u].sz < x) return 0;//如果不存在x个,那么就输出0
while (1) {
int lc = tr[u].ch[0];//左儿子
if (x > tr[lc].sz + tr[u].cnt) {//情况1
x -= tr[lc].sz + tr[u].cnt;
u = tr[u].ch[1];//跳右儿子
}
else {
if (tr[lc].sz >= x) u = lc;//如果左儿子节点的个数足够,那么就到左儿子上面查找
else return tr[u].val;//不然就是在当前的节点上
}
}
}
}sl;
int main() {
int n; read(n);
sl.ins(-inf);
sl.ins(inf);
for (int _t = 1; _t <= n; _t ++) {
int opt, x; read(opt); read(x);
if (opt == 1) sl.ins(x);
if (opt == 2) sl.del(x);
if (opt == 3) {
sl.find(x);
printf("%d\n", sl.tr[sl.tr[sl.rt].ch[0]].sz);
}
if (opt == 4) printf("%d\n", sl.kth(x + 1));
if (opt == 5) printf("%d\n", sl.tr[sl.pre(x)].val);
if (opt == 6) printf("%d\n", sl.tr[sl.suc(x)].val);
}
return 0;
}
平衡树splay学习笔记#2的更多相关文章
- 平衡树splay学习笔记#1
这一篇博客只讲splay的前一部分的操作(rotate和splay),后面的一段博客咕咕一段时间 后一半的博客地址:[传送门] 前言骚话 为了学lct我也是拼了,看了十几篇博客,学了将近有一周,才A掉 ...
- 文艺平衡树 Splay 学习笔记(1)
(这里是Splay基础操作,reserve什么的会在下一篇里面讲) 好久之前就说要学Splay了,结果苟到现在才学习. 可能是最近良心发现自己实在太弱了,听数学又听不懂只好多学点不要脑子的数据结构. ...
- 【洛谷P3369】普通平衡树——Splay学习笔记(一)
二叉搜索树(二叉排序树) 概念:一棵树,若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值: 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值: 它的左.右子树也分别为二叉搜索树 ...
- 【洛谷P3391】文艺平衡树——Splay学习笔记(二)
题目链接 Splay基础操作 \(Splay\)上的区间翻转 首先,这里的\(Splay\)维护的是一个序列的顺序,每个结点即为序列中的一个数,序列的顺序即为\(Splay\)的中序遍历 那么如何实现 ...
- [Splay][学习笔记]
胡扯 因为先学习的treap,而splay与treap中有许多共性,所以会有很多地方不会讲的很细致.关于treap和平衡树可以参考这篇博客 关于splay splay,又叫伸展树,是一种二叉排序树,它 ...
- [Note]Splay学习笔记
开个坑记录一下学习Splay的历程. Code 感谢rqy巨佬的代码,让我意识到了Splay可以有多短,以及我之前的Splay有多么的丑... int fa[N], ch[N][2], rev[N], ...
- splay学习笔记
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.(来自百科) 伸展树的操作主要是 –rotate(x) 将x旋转到x的父亲的位置 voi ...
- Treap-平衡树学习笔记
平衡树-Treap学习笔记 最近刚学了Treap 发现这种数据结构真的是--妙啊妙啊~~ 咳咳.... 所以发一发博客,也是为了加深蒟蒻自己的理解 顺便帮助一下各位小伙伴们 切入正题 Treap的结构 ...
- [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家
1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...
随机推荐
- Debian搭建WordPress
环境配置 可以使用apt-get快速安装mysql,php5:我是用源码手动安装apache服务器的.安装完mysql后,最好将字符编码设置为utf8的. 接下来就是mysql,apache,php5 ...
- 合并dll文件
在使用VS进行.Net编程时,出现了一个奇怪的现象. 在一个类库项目中导入了dll库A后,再导入A的两个依赖项(dll库)B和C,执行“生成”操作时,出现错误信息,提示B和C的库版本与A所需的不一致. ...
- Git更新本地仓库
1.查看远程仓库git remote -v2.从远程获取最新版本到本地git fetch origin master:temp3.比较本地的仓库与远程仓库的区别git diff temp4.合并tem ...
- [2017BUAA软工助教]个人项目准备工作
BUAA软工个人项目准备工作 零.注册Github个人账号(你不会没有吧..) 这是Git的使用教程: http://www.cnblogs.com/schaepher/p/5561193.html ...
- 【问题解决方案】Git bash进入多层子目录问题(通配符问题留坑)
cd进入指定路径下:cd 斜杠 斜杠 方法一: 1- 撇丿,不是"那",盘符前面要加上 / (d盘前面也加,不加也行) 2- 路径名不区分大小写 3- 不用空格 4- 如果目录名中 ...
- symfony框架
Symfony是一个完整的框架结构,设计用来帮助并加速网络应用的开发. 1)安装 symfony的安装还是比较简单的,而且提供了多种安装的方式,详情可以看官网手册 问题: cURL error 60: ...
- Webbench、ab命令:做压力测试的工具和性能的监控工具
DDOS攻击:???DDOS概述:分布式拒绝服务(DDoS:Distributed Denial of Service)攻击,指借助于客户/服务器技术,将多个计算机联合起来作为攻击平台,对一个或多个目 ...
- git fetch 和git pull 的差别
1.git fetch 相当于是从远程获取最新到本地,不会自动merge,如下指令: git fetch orgin master //将远程仓库的master分支下载到本地当前branch中 git ...
- day 7-7 线程池与进程池
一. 进程池与线程池 在刚开始学多进程或多线程时,我们迫不及待地基于多进程或多线程实现并发的套接字通信,然而这种实现方式的致命缺陷是:服务的开启的进程数或线程数都会随着并发的客户端数目地增多而增多,这 ...
- vue.js实战——vue 实时时间
created:实例创建完成后调用,此阶段完成了数据的观测等,但尚未挂载,$el还不可用,需要初始化处理一些数据时会比较有用. mounted:el挂载到实例上后调用,一般我们的第一个业务逻辑会在这里 ...