平衡树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.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...
随机推荐
- Python之字符串操作
一.字符串特点 内容不可修改 password=' #内容不可修改 二.字符串常用方法 1..strip()方法 去字符串两边的空格和换行符 print(password.strip()) #去掉字符 ...
- Linux 安装软件之后设置PATH环境变量
每一个软件都有安装路径这一项,指定安装路径的目的,一方面是便于文件搜索与查找,另一方面更方便的使用软件. 比如,几乎大多数自己安装的软件,都会选择安装在/usr/local目录下,比如apache.m ...
- MySQL中有关NULL的计算
mysql> select NULL=NULL; #判断两个NULL是否相等,结果不是1也不是0 +-----------+ | NULL=NULL | +-----------+ | NULL ...
- 爬虫——scrapy框架
Scrapy是一个异步处理框架,是纯Python实现的爬虫框架,其架构清晰,模块之间的耦合程度低,可拓展性强,可以灵活完成各种需求.我们只需要定制几个模块就可以轻松实现一个爬虫. 1.架构 Scra ...
- MySQL的SQL语句优化-group by语句的优化
原文:http://bbs.landingbj.com/t-0-243202-1.html 默认情况下,MySQL排序所有GROUP BY col1, col2, ....,查询的方法如同在查询中指定 ...
- java注解和自定义注解的简单使用
前言 在使用Spring Boot的时候,大量使用注解的语法去替代XML配置文件,十分好用. 然而,在使用注解的时候只知道使用,却不知道原理.直到需要用到自定义注解的时候,才发现对注解原理一无所知,所 ...
- Linux的基本解读
Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户.多任务.支持多线程和多CPU的操作系统 而严格来讲,Linux这个词本身只表示Linux内核,但实际上人 ...
- nginx配置ssl证书后无法访问https
一直听说https更安全,要安装证书,一直没试过,今天终于试了试 首先得有个http的域名网站,服务器. 到阿里云的安全-ssl证书管理申请一个免费的,可以绑定一个域名 然后完善资料,照着例子配置一 ...
- Servlet--HttpServlet实现doGet和doPost请求的原理
转:https://blog.csdn.net/m0_38039437/article/details/75264012 一.HttpServlet简介 1.HttpServlet是GenericSe ...
- [转帖]Linux分页机制之概述--Linux内存管理(六)
Linux分页机制之概述--Linux内存管理(六) 2016年09月01日 19:46:08 JeanCheng 阅读数:5491 标签: linuxkernel内存管理分页架构更多 个人分类: ┈ ...