快速入门Splay
\(splay\) :伸展树(\(Splay Tree\)),也叫分裂树,是一种二叉排序树,它能在\(O(log n)\)内完成插入、查找和删除操作。它由\(Daniel Sleator\)和\(Robert Tarjan\)创造,后勃刚对其进行了改进。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作。
先让我们看一下一棵二叉搜索树(\(Binary\) \(Search\) \(Tree\))是什么样子的。

如图所示,对任意一棵\(BST\),它有以下性质:
- 是一棵空树,或者是具有下列性质的二叉树
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
根据定义,我们会发现:
- 这棵树的中序遍历,与其压成数组的升序排序等效
在这样一棵树中,我们可以很容易地维护以下信息:
- 查询\(x\)数的排名
- 查询排名为\(x\)的数
- 求\(x\)的前驱(前驱定义为小于\(x\),且最大的数)
- 求\(x\)的后继(后继定义为大于\(x\),且最小的数)
同样的我们会发现,对于一个固定的数列,它可以形成很多种不同类型的\(BST\)。如果这棵树恰好不太优美,每次维护的复杂度可能会被卡到\(O(N)\)(一条链)。
所以,平衡树这种伟大的数据结构就诞生啦!
顾名思义,平衡树就是一棵可以保持全树平衡的二叉搜索树,以此避免复杂度退化为\(O(N)\)。比较经典的一种平衡树是\(Treap\),它基于的是对一个有序数列,随机出的\(BST\)期望复杂度是\(O(logN)\),通过利用堆的性质来维护其随机性,这个东西我的上一篇博客已经介绍过,不再展开介绍。今天我们要介绍的是另一种经典的平衡树——\(Splay\)。
既然是平衡树,\(Splay\)是如何实现其树体平衡的呢?
在\(Splay\)的每一个维护操作中,维护结束后当前被维护的点都会被旋转成为树的根节点,这个过程叫做树的伸展。\((Splay)\)。伸展是\(Splay\)的核心操作。与\(Treap\)利用随机出来的优先级进行堆的维护不同,\(Splay\)的大多数操作都要基于伸展操作,这也决定了\(Splay\)相比前者具有更广泛的适用性。
那\(Splay\)是怎么保证其复杂度不退化成\(O(N)\)的呢?来看个例子。

在这一棵已经退化成链的\(BST\)中,我们对最底下那个节点进行了一次维护。在这之后,这个节点就开始了向根节点的漫漫伸展之路~
所以在伸展过程结束后,这棵树就再次自发地进化回了一棵正常的树。如果深度更深会更加明显,在一次\(Splay\)以后,它会从\(N\)级别的深度进化为\(logN\)级别。
接着让我们贪心地想一想,假如现在这棵树非常的不优秀。我想要把它卡掉,就应该总是访问它最不优秀的节点。如果最开始它还有很多超级长的链,那么经过几次贪心的访问之后,它的所有链中的最大深度就已经回到\(logN\)了。不管常数怎么样,均摊一下复杂度是没有问题了。
既然这些操作\(Treap\)也能做,为什么不用又快又好写的\(Treap\)呢?因为\(Splay\)在区间操作和\(LCT\)中有其不可替代的作用。具体是什么作用,我也没有学到,等到学了在拿出来讲吧QwQ
讲过了原理,我们可以来看一下代码实现了Qw
inline void push_up (int u) {
t[u].sz = t[u].cnt;
t[u].sz += t[t[u].ch[0]].sz;
t[u].sz += t[t[u].ch[1]].sz;
}
inline void rotate (int x) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
connect (z, x, d2);
connect (y, t[x].ch[!d1], d1);
connect (x, y , !d1);
push_up (y);
push_up (x);
}
这里\(connect\)是一个连边的函数,旋转的原理和\(Treap\)一样,都是要保证其\(BST\)的性质,可以手画一下示意图就明白啦~
inline void splay (int x, int goal) {
if (x == 0) return;
while (t[x].fa != goal) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
if (z != goal) {
if (d1 == d2) {
rotate (y);
} else {
rotate (x);
}
}
rotate (x);
}
if (goal == 0) {
root = x;
}
}
核心操作——伸展,可以思考一下:为什么是把\(x\)旋转为\(goal\)的子节点?
剩下的操作,作者很懒,就只贴上代码啦~
inline void find (int key) {
int u = root;
if (u == 0) return;
while (t[u].key != key && t[u].ch[key > t[u].key]) {
u = t[u].ch[key > t[u].key];
}
//找到key对应的节点,并把它旋转到根。
splay (u, 0);
}
inline void Insert (int key) {
int u = root, fa = 0;
while (u != 0 && t[u].key != key) {
fa = u;//记得记录父亲
if (key > t[u].key) {
u = t[u].ch[1];
} else {
u = t[u].ch[0];
}
}
if (u != 0) {
//已有(能查到)
++t[u].sz;
++t[u].cnt;
} else {
//新增
u = ++max_size;
t[u].sz = 1;
t[u].cnt = 1;
t[u].key = key;
connect (fa, u, key > t[fa].key);
}
splay (u, 0);
}
inline int Next (int key, int dir) {
//dir = 0 -> 前驱
//dir = 1 -> 后继
find (key);
int u = root;
if (dir == 0 && t[u].key < key) return u;
if (dir == 1 && t[u].key > key) return u;
//如果key值并没有存在于树中:
u = t[u].ch[dir];
while (t[u].ch[!dir]) {
u = t[u].ch[!dir];
}
//e.g 如果要找前驱,就先往左一步(保证一定比当前值更小),再一直向右(最大的那个)。
return u;
}
inline void Delete (int key) {
int _pre = Next (key, 0);
int _nxt = Next (key, 1);
splay (_pre, 0000);
splay (_nxt, _pre);
//当前键值key的前驱是_pre, 后继是_nxt
//_pre被旋转到根节点,_nxt成为_pre的子节点(显然是右)
//那么当前点一定在_nxt的左边,而且底下没有任何一个点。
int u = t[_nxt].ch[0];
if (t[u].cnt > 1) {
--t[u].cnt;
splay (u, 0);
} else {
t[_nxt].ch[0] = 0;
}
}
inline int kth (int k) {
int u = root;
if (u == 0) return 0;
while (u != 0) {
int ls = t[u].ch[0];
int rs = t[u].ch[1];
if (k > t[ls].sz + t[u].cnt) {
k -= t[ls].sz + t[u].cnt;
u = rs;//格外注意不要写反顺序
} else if (k <= t[ls].sz) {
u = ls;
} else {
return t[u].key;
}
}
return false;
}
inline int get_rnk (int key) {
find (key);
return t[t[root].ch[0]].sz;
}
还有一点需要注意的,\(splay\)在使用前要先\(insert\)一个极大值和一个极小值。否则在\(Next\)函数的查找中,比如只有一个点的话,会出现找不到前驱和后继的情况,也就会导致出莫名其妙的锅。当然,加上极大极小值之后要格外注意对答案的处理。下面给出完整代码,题目P3369 【模板】普通平衡树。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100010
#define INF 0x7fffffff
using namespace std;
struct Splay_Tree {
int root, max_size;
struct Splay_Node {
int sz, fa, cnt, key, ch[2];
}t[N];
Splay_Tree () {
root = max_size = 0;
memset (t, 0, sizeof (t));
}
inline void connect (int u, int v, int dir) {
t[u].ch[dir] = v;
t[v].fa = u;
}
inline void push_up (int u) {
t[u].sz = t[u].cnt;
t[u].sz += t[t[u].ch[0]].sz;
t[u].sz += t[t[u].ch[1]].sz;
}
inline void rotate (int x) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
connect (z, x, d2);
connect (y, t[x].ch[!d1], d1);
connect (x, y , !d1);
push_up (y);
push_up (x);
}
inline void splay (int x, int goal) {
if (x == 0) return;
while (t[x].fa != goal) {
int y = t[x].fa;
int z = t[y].fa;
int d1 = t[y].ch[1] == x;
int d2 = t[z].ch[1] == y;
if (z != goal) {
if (d1 == d2) {
rotate (y);
} else {
rotate (x);
}
}
rotate (x);
}
if (goal == 0) {
root = x;
}
}
inline void find (int key) {
int u = root;
if (u == 0) return;
while (t[u].key != key && t[u].ch[key > t[u].key]) {
u = t[u].ch[key > t[u].key];
}
splay (u, 0);
}
inline void Insert (int key) {
int u = root, fa = 0;
while (u != 0 && t[u].key != key) {
fa = u;
if (key > t[u].key) {
u = t[u].ch[1];
} else {
u = t[u].ch[0];
}
}
if (u != 0) {
++t[u].sz;
++t[u].cnt;
} else {
u = ++max_size;
t[u].sz = 1;
t[u].cnt = 1;
t[u].key = key;
connect (fa, u, key > t[fa].key);
}
splay (u, 0);
}
inline int Next (int key, int dir) {
find (key);
int u = root;
if (dir == 0 && t[u].key < key) return u;
if (dir == 1 && t[u].key > key) return u;
u = t[u].ch[dir];
while (t[u].ch[!dir]) {
u = t[u].ch[!dir];
}
return u;
}
inline void Delete (int key) {
int _pre = Next (key, 0);
int _nxt = Next (key, 1);
splay (_pre, 0000);
splay (_nxt, _pre);
int u = t[_nxt].ch[0];
if (t[u].cnt > 1) {
--t[u].cnt;
splay (u, 0);
} else {
t[_nxt].ch[0] = 0;
}
}
inline int kth (int k) {
int u = root;
if (u == 0) return 0;
while (u != 0) {
int ls = t[u].ch[0];
int rs = t[u].ch[1];
if (k > t[ls].sz + t[u].cnt) {
k -= t[ls].sz + t[u].cnt;
u = rs;//格外注意
} else if (k <= t[ls].sz) {
u = ls;
} else {
return t[u].key;
}
}
return false;
}
inline int get_rnk (int key) {
find (key);
return t[t[root].ch[0]].sz;
}
}st;
int n, x, opt;
int main () {
// freopen ("splay.in", "r", stdin);
scanf ("%d", &n);
st.Insert (+INF);
st.Insert (-INF);
for (int i = 1; i <= n; ++i) {
scanf ("%d %d", &opt, &x);
if (opt == 1) {
st.Insert (x);
}
if (opt == 2) {
st.Delete (x);
}
if (opt == 3) {
printf ("%d\n", st.get_rnk (x));
}
if (opt == 4) {
printf ("%d\n", st.kth (x + 1));
}
if (opt == 5) {
printf ("%d\n", st.t[st.Next (x, 0)].key);
}
if (opt == 6) {
printf ("%d\n", st.t[st.Next (x, 1)].key);
}
}
}
快速入门Splay的更多相关文章
- Web Api 入门实战 (快速入门+工具使用+不依赖IIS)
平台之大势何人能挡? 带着你的Net飞奔吧!:http://www.cnblogs.com/dunitian/p/4822808.html 屁话我也就不多说了,什么简介的也省了,直接简单概括+demo ...
- SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=》提升)
SignalR快速入门 ~ 仿QQ即时聊天,消息推送,单聊,群聊,多群公聊(基础=>提升,5个Demo贯彻全篇,感兴趣的玩才是真的学) 官方demo:http://www.asp.net/si ...
- 前端开发小白必学技能—非关系数据库又像关系数据库的MongoDB快速入门命令(2)
今天给大家道个歉,没有及时更新MongoDB快速入门的下篇,最近有点小忙,在此向博友们致歉.下面我将简单地说一下mongdb的一些基本命令以及我们日常开发过程中的一些问题.mongodb可以为我们提供 ...
- 【第三篇】ASP.NET MVC快速入门之安全策略(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- Mybatis框架 的快速入门
MyBatis 简介 什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架.MyBatis 消除 了几乎所有的 JDBC 代码和参数的手工设置以及结果 ...
- grunt快速入门
快速入门 Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. Grunt 0.4.x 必须配合Node.js >= 0.8.0版本使用.:奇数版本 ...
- 【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- 【第四篇】ASP.NET MVC快速入门之完整示例(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
随机推荐
- SpringBoot之文件下载
package org.springboot.controller; import org.springboot.constant.Constant; import org.springframewo ...
- 四、docker compose
docker compose可以方便我们快捷高效地管理容器的启动.停止以及重启等操作,和批量管理容器,它类似于linux下的shell脚本,基于yaml语法,在该文件里我们可以描述应用的架构,比如用什 ...
- codeforces618B
Guess the Permutation CodeForces - 618B Bob has a permutation of integers from 1 to n. Denote this p ...
- 一、Java多人博客系统-开篇
作为一个程序员,工作之外的不断学习是必须的.这个项目是我个人课外学习和练手的项目.最开始是一个个人网站.当时发现京东云可以免费部署网站的,就立即写了一个网站,当时就使用jsp技术,可以实现发布博客.评 ...
- 【NLP】大白话讲解word2vec到底在做些什么
转载自:http://blog.csdn.net/mylove0414/article/details/61616617 词向量 word2vec也叫word embeddings,中文名“词向量”, ...
- MVC 动态菜单
直接上代码: 一,创建菜单 Action public ActionResult GetMenu() { //获取菜单 List<MenuItem> mainMenu = mm.GetMe ...
- 洛谷P1119灾后重建
题目 做一个替我们首先要明确一下数据范围,n<=200,说明n^3的算法是可以过得,而且这个题很明显是一个图论题, 所以我们很容易想到这个题可以用folyd, 但是我在做这个题的时候因为没有深刻 ...
- Git的搭建
Git的搭建 第1步:官网下载安装git 第2步:github官网注册账号 第3步:配置git 第4步:github这是秘钥 第5步:上传本地工程到git 主要参考的博客(这三篇博客能让你顺利上传至g ...
- swagger2 如何匹配多个controller
方法一:使用多个controller的共同拥有的父类,即精确到两个controller的上一级 @Bean public Docket createRestApi() { return new Doc ...
- 前后端分离之vue2.0+webpack2 实战项目 -- html模板拼接
对于前后端分离,如何把一个页面的公共部分比如head, header, footer, content等组合成一个完整的html 是一个值得考虑的地方. 对于php,我们可以利用include加载其他 ...