Splay简介
Splay树,又叫伸展树,可以实现快速分裂合并一个序列,几乎可以完成平衡树的所有操作。其中最重要的操作是将指定节点伸展到指定位置,
节点定义
一棵普通的splay并不需要什么太多的附加数据,就像下面这样就好:
template<typename T>
class SplayNode {
public:
T data;
SplayNode* next[];
SplayNode* father;
SplayNode(){
memset(next, , sizeof(next));
}
SplayNode(T data, SplayNode* father):data(data), father(father){
memset(next, , sizeof(next));
}
int cmp(T a){
if(a == data) return -;
return (a > data) ? () : ();
}
};
伸展操作
伸展操作有三种情况,分为单旋转(一种情况)和双旋转(二种情况)
当伸展的节点的父节点为目标位置,那么只需要一次旋转就可以完成。和这个方向相反(如果为左子树,就右旋)
例如将开篇那张图中键值为3的点,伸展到根。- 要伸展的节点的父节点和祖父节点共线,则先将父节点转上去,再将该节点转上去,例如上图,将键值为9的节点伸展到根。
- 第三种情况是要伸展的节点的父节点和祖父节点不共线(呈"之"字),此时先将该节点连续旋转两次达到祖父节点的位置。例如将第一张图的键值为6的节点伸展到根。
基本所有题目的数据范围都不至于使一次单旋转或双旋转就能够解决,所以实际中是通过三种情况组合进行伸展。
比如说将某个深度较深的节点伸展到根,会发现不光是这个节点的深度更小了(更浅),很多其它节点也有所受益。最坏的情况是O(n)(从小到大的数据中最小的一个伸展到根),最好的情况O(1)(刚好是父节点的直接的某个子树),平均是O(log2n)(我也不知道怎么算的,反正实际用起来绝对比这个慢,或者说常数很大,因为Splay不像AVL树和红黑树那样特别平衡)。
您可以考虑在插入、查找的过程中把结果伸展到根。
下面是代码:
inline void splay(SplayNode<T>* node, SplayNode<T>* father){
while(node->father != father){
SplayNode<T>* f = node->father;
int fd = f->cmp(node->data);
SplayNode<T>* ff = f->father;
if(ff == father){
rotate(f, fd ^ );
break;
}
int ffd = ff->cmp(f->data);
if(ffd == fd){
rotate(ff, ffd ^ );
rotate(f, fd ^ );
}else{
rotate(f, fd ^ );
rotate(ff, ffd ^ );
}
}
if(father == NULL)
root = node;
}
插入操作
Splay的插入操作很简单,按照BST的性质插进去,然后伸展到根。
//实际过程
static SplayNode<T>* insert(SplayNode<T>*& node, SplayNode<T>* father, T data){
if(node == NULL){
node = new SplayNode<T>(data, father);
return node;
}
int d = node->cmp(data);
if(d == -) return NULL;
return insert(node->next[d], node, data);
} //用户调用
inline SplayNode<T>* insert(T data){
SplayNode<T>* res = insert(root, NULL, data);
if(res != NULL) splay(res, NULL);
return res;
}
删除操作
虽然Treap的删除貌似在这也可以借鉴一下,但是还是希望用到splay函数。
比如开始那张图,要删除键值为7的节点,那么先把它伸展到根:
如果某棵子树为空,那么直接删掉就好了,然后改下root。
先假设键值为6的节点不存在,那么直接用键值为5的节点来代替根节点就好了。可是事实上有键值为6的节点。那么想一种情况根节点的左子树的右子树为空的情况。很巧根据BST的性质(设根节点为x,根节点的左子树为y,y的右子树为z),那么x > z > y。如果不存在z,也就是说y是x的前驱(小于x且最大的数)。
根据前驱的定义,可以写出一下找根节点的前驱的代码。
SplayNode<T>* maxi = re->next[];
while(maxi->next[] != NULL) maxi = maxi->next[];
找到前驱然后伸展到根节点的左子树。最后的结果图:
实际应用时通常会加入永远都不可能被删掉的最小的一个节点(哨兵节点),这样根就不存在要删的节点的左子树为空的情况。所以可以省下一些代码。
代码:
inline boolean remove(T data){
SplayNode<T>* re = find(data);
if(re == NULL) return false;
SplayNode<T>* maxi = re->next[];
if(maxi == NULL){
root = re->next[];
if(re->next[] != NULL) re->next[]->father = NULL;
delete re;
return true;
}
while(maxi->next[] != NULL) maxi = maxi->next[];
splay(maxi, re);
maxi->next[] = re->next[];
if(re->next[] != NULL) re->next[]->father = maxi;
maxi->father = NULL;
delete re;
root = maxi;
return true;
}
前驱后继操作
首先把要求前驱或后继的节点伸展到根。然后很容易就想到。
inline SplayNode<T>* findPre(SplayNode<T>* node) {
if(node != root) splay(node, NULL);
SplayNode<T>* s = node->next[];
while(s != NULL && s->next[] != NULL) s = s->next[];
return s;
} inline SplayNode<T>* findSuf(SplayNode<T>* node) {
if(node != root) splay(node, NULL);
SplayNode<T>* s = node->next[];
while(s != NULL && s->next[] != NULL) s = s->next[];
return s;
}
如果不是该树内的节点,后继用upper_bound,前驱就用自创的less_bound。思路和upper_bound差不多。
static SplayNode<T>* less_bound(SplayNode<T>*& node, T val){
if(node == NULL) return node;
int to = node->cmp(val);
if(val == node->data) to = ;
SplayNode<T>* ret = less_bound(node->next[to], val);
return (ret == NULL && node->data < val) ? (node) : (ret);
} SplayNode<T>* less_bound(T data){
SplayNode<T>* p = less_bound(root, data);
if(p != NULL)
splay(p, NULL);
return p;
}
可重Splay
·节点定义
既然让Splay支持重复的内容,那么就要加入一个count。因为根据BST的性质,新加入的重复的节点,放哪都会破坏性质(因为都是大于或小于),所以只好加在原先节点的头上
template<typename T>
class SplayNode {
public:
T data;
int count; //这里
SplayNode* next[];
SplayNode* father;
SplayNode(){
9 memset(next, , sizeof(next));
}
SplayNode(T data, SplayNode* father):data(data), father(father), count(){
memset(next, , sizeof(next));
}
int cmp(T a){
if(a == data) return -;
return (a > data) ? () : ();
}
void addCount(int val){ //这里
this->count += val;
}
};
·插入 & 删除操作
插入特判是否有重复的键值,删除特判count为0.(即是否需要移除节点)
名次操作
要进行名次操作(K小值和x的排名)对于一个节点,要做到O(log2n) 就要想办法通过某些手段不做一些无用的访问。这时可以考虑加入一个s(size)附加数据,记录该子树上的节点(数据,包括重复的内容)个数。
而且旋转后某些节点的s需要改变,所以需要一个维护s的函数
template<typename T>
class SplayNode {
public:
T data;
int s; //这里
int count;
SplayNode* next[];
SplayNode* father;
//.......
void maintain(){ //这里
s = count;
for(int i = ; i < ; i++)
if(next[i] != NULL)
s += next[i]->s;
}
void addCount(int val){
this->s += val;
this->count += val; //这里
}
};
旋转时,如何确定待维护节点?先看一下下图(怎么感觉两张图都有在树链剖分)
由此可以得出规律,旧的"根节点"和新的"根节点"需要维护。
inline static void rotate(SplayNode<T>*& node, int d){
//.......
node->maintain();
node->father->maintain();
}
首先来说求K小值吧,从根节点开始访问(这不是废话吗),然后确定左子树的个数ls,如果左子树为空,那么就记为0。
很容易就能想到一个节点的左子树的个数为ls个,那么以这个节点为根的树上,根的排名是(ls + 1)名。于是我们可以得出递归的边界(写成while也行)
if(k >= ls + && k <= ls + node->count) return node;
如果访问左子树(k <= ls),那么没有什么特别的。但是如果访问右子树,你现在要求的右子树上的第某小值,而k是对于当前的node来说,所以应该减去s和node->count。
K小值代码:
static SplayNode<T>* findKth(SplayNode<T>*& node, int k){
int ls = (node->next[] != NULL) ? (node->next[]->s) : ();
if(k >= ls + && k <= ls + node->count) return node;
if(k <= ls) return findKth(node->next[], k);
return findKth(node->next[], k - ls - node->count);
} inline SplayNode<T>* findKth(int k){
if(k <= || k > root->s) return NULL;
SplayNode<T>* p = findKth(root, k);
splay(p, NULL);
return p;
}
求x的排名就很简单了。还是访问,比当前节点小,访问左子树,相等返回r + 1,否则访问右子树,r加上当前节点的左子树的大小和count。如果访问到了NULL,返回r + 1。
为什么返回的都是r + 1呢?
因为加的左子树的大小等都是确定比它小的节点的个数。
下面是代码:
inline int rank(T data){
SplayNode<T>* p = root;
int r = ;
while(p != NULL){
int ls = (p->next[] != NULL) ? (p->next[]->s) : ();
if(p->data == data) return r + ls + ;
int d = p->cmp(data);
if(d == ) r += ls + p->count;
p = p->next[d];
}
return r + ;
}
区间操作
·split(int from, int end)
从原树中分离出一段区间[from, end]。
和之前删除的思想一样,调用splay函数使某个(没错,就是一个)特定的子树就是这一个区间。这里不好想,我就直接说吧。
/*
* 先找到第(end + 1)名,然后伸展到根,然后找到(from - 1)名,伸展到根的左子树。然后根的左子树的右子树就是这个区间了。
* 当然(end + 1)和(from - 1)都不一定会存在,所以特判或者加入哨兵节点。
*/
SplayNode<T>* split(int from, int end){
if(from > end) return NULL;
if(from == && end == root->s){
findKth(, NULL);
return this->root;
}
if(from == ){
findKth(end + , NULL);
findKth(from, root);
return root->next[];
}
if(end == root->s){
findKth(from - , NULL);
findKth(end, root);
return root->next[];
}
findKth(end + , NULL);
findKth(from - , root);
return root->next[]->next[];
}
分离区间
不过通常都需要用Splay来处理字符串等,这些是按照数组下标来建立Splay。翻转也是家常便饭,因此只能靠访问的顺序来当成"下标"(翻转后很难修改记录的下标)。
至于翻转操作就打lazy标记,然后建立一个pushDown()函数
void pushDown(){
swap(next[], next[]);
for(int i = ; i < ; i++)
if(next[i] != NULL)
next[i]->lazy ^= ;
lazy = false;
}
就像这样,很多区间操作都可以做。
Splay简介的更多相关文章
- [数据结构]Splay简介
Splay树,又叫伸展树,可以实现快速分裂合并一个序列,几乎可以完成平衡树的所有操作.其中最重要的操作是将指定节点伸展到指定位置, 目录 节点定义 旋转操作 伸展操作 插入操作 删除操作 lower_ ...
- splay详解(一)
前言 Spaly是基于二叉查找树实现的, 什么是二叉查找树呢?就是一棵树呗:joy: ,但是这棵树满足性质—一个节点的左孩子一定比它小,右孩子一定比它大 比如说 这就是一棵最基本二叉查找树 对于每次插 ...
- 【BZOJ】1507: [NOI2003]Editor(Splay)
http://www.lydsy.com/JudgeOnline/problem.php?id=1507 当练splay模板了,发现wjmzbmr的splay写得异常简介,学习了.orzzzzzzzz ...
- [模板] 平衡树: Splay, 非旋Treap, 替罪羊树
简介 二叉搜索树, 可以维护一个集合/序列, 同时维护节点的 \(size\), 因此可以支持 insert(v), delete(v), kth(p,k), rank(v)等操作. 另外, prev ...
- 平衡树简单教程及模板(splay, 替罪羊树, 非旋treap)
原文链接https://www.cnblogs.com/zhouzhendong/p/Balanced-Binary-Tree.html 注意是简单教程,不是入门教程. splay 1. 旋转: 假设 ...
- [洛谷日报第62期]Splay简易教程 (转载)
本文发布于洛谷日报,特约作者:tiger0132 原地址 分割线下为copy的内容 [洛谷日报第62期]Splay简易教程 洛谷科技 18-10-0223:31 简介 二叉排序树(Binary Sor ...
- Splay模板讲解及一些题目
普通平衡树模板以及文艺平衡树模板链接. 简介 平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二 ...
- 伸展树(Splay Tree)进阶 - 从原理到实现
目录 1 简介 2 基础操作 2.1 旋转 2.2 伸展操作 3 常规操作 3.1 插入操作 3.2 删除操作 3.3 查找操作 3.4 查找某数的排名.查找某排名的数 3.4.1 查找某数的排名 3 ...
- [学习笔记]平衡树(Splay)——旋转的灵魂舞蹈家
1.简介 首先要知道什么是二叉查找树. 这是一棵二叉树,每个节点最多有一个左儿子,一个右儿子. 它能支持查找功能. 具体来说,每个儿子有一个权值,保证一个节点的左儿子权值小于这个节点,右儿子权值大于这 ...
随机推荐
- 如何在Ubuntu 14.10 上安装WordPress?
http://codex.wordpress.org/zh-cn:安装WordPress 介绍 如果你想快捷.简单.免费的创建个人网站的话,WordPress 是你最佳的选择. WordPress 是 ...
- SET NAMES
High Performance MySQL, Third Editionby Baron Schwartz, Peter Zaitsev, and Vadim Tkachenko Settings ...
- 修改机器名、IP对arcgis server、portal的影响?
修改机器名.IP是否对ArcGIS Server .Portal等有影响? 请教赛姐:修改IP对ArcGIS Server .Portal 无影响,不过建议将ArcGIS Server .Portal ...
- 运行Chromium浏览器无法登录谷歌账号因为缺少google api密钥
管理员身份运行CMD,然后依次输入以下三行内容(因为比较长,最好拷贝,因为某原因粘贴第一行后,我输入了下,结果第一次没成功,可能是输入错误了.): setx GOOGLE_API_KEY " ...
- Alisha’s Party---hdu5437(模拟+优先队列)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5437 题意:公主有k个朋友来参加她的生日party,每个人都会带价值为v[i]的礼物过来,在所有人到齐 ...
- compass利用koala在chrome开启scss调试
compass不生成maps文件,所载调试css上,极不方便.看到下图的调试方式,怎么做. 利用用koala来解决,具体步骤如下: 1.确保自己安装了ruby和sass,compass.接着安装 co ...
- qt——QT中QWidget、QDialog及QMainWindow的区别
QWidget类是所有用户界面对象的基类. 窗口部件是用户界面的一个基本单元:它从窗口系统接收鼠标.键盘和其它事件,并且在屏幕上绘制自己.每一个窗口部件都是矩形的,并且它们按Z轴顺序排列.一个窗口部件 ...
- 【Python】【Web.py】python web py入门-5-请求处理(下)
前面一篇,我们演示了如何获取GET和POST请求的参数信息,这篇我们介绍如何获取请求的头部信息,这个方法我们在前面一篇文章已经给出了.直接来看一个例子,首先,我们在hello.py文件新增一个方法,用 ...
- mysql 表基本增删查改
对表的操作是在某个数据库下才能进行的,所以要先选择数据库 "use 数据库名;" 1.创建数据表 1)"create table 表名 (字段1 类型 [约束], ...
- Python3学习之路~2.5 简单的三级菜单程序
程序:三级菜单 需求: 1.打印省.市.县三级菜单2.可返回上一级3.可随时退出程序 代码1: data={ "山东":{ "济南":["历下区&qu ...