前几天由于出行计划没有更博QwQ

(其实是因为调试死活调不出来了TAT我好菜啊)

伸展树

伸展树(英语:Splay Tree)是一种二叉查找树,它能在O(log n)内完成插入、查找和删除操作。它是由丹尼尔·斯立特(Daniel Sleator)和罗伯特·塔扬在1985年发明的[1]

在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行调整,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

它的优势在于不需要记录用于平衡树的冗余信息。

优点

  • 可靠的性能——它的平均效率不输于其他平衡树[2]
  • 存储所需的内存少——伸展树无需记录额外的什么值来维护树的信息,相对于其他平衡树,内存占用要小。

缺点

伸展树最显著的缺点是它有可能会变成一条。这种情况可能发生在以非降顺序访问n个元素之后。然而均摊的最坏情况是对数级的——O(log n)

以上摘自中文Wikipedia

永远不要用单旋代替双旋...单旋那叫Spaly,Splay中的势能分析在单旋时会失效,复杂度不对的...(警告某整天单旋的ryf)

Rotate操作:

离散数学中,树旋转(英语:Tree rotation)是在二叉树中的一种子树调整操作, 每一次旋转并不影响对该二叉树进行中序遍历的结果. 树旋转通常应用于需要调整树的局部平衡性的场合。

然后上几张图来作一下左旋/右旋的说明:

(第一张是动图不知道cnblogs能不能很好地滋磁GIF)

我们可以将左旋理解为将根旋转为右子节点的左子树,右旋为将根旋转为左子节点的右子树(往哪旋根就变成哪边的子树)

附C++袋马实现(左右合并,k=0为左旋,k=1为右旋)

#define lch chd[0]
#define rch chd[1]
#define kch chd[k]
#define xch chd[k^1] void Rotate(Node* root,int k){
Node* tmp=root->xch;
if(root->prt==NULL)
this->root=tmp;
else if(root->prt->lch==root)
root->prt->lch=tmp;
else
root->prt->rch=tmp;
tmp->prt=root->prt;
root->xch=tmp->kch;
if(root->xch!=NULL)
root->xch->prt=root;
tmp->kch=root;
root->prt=tmp;
}

Rotate

注:Node的定义:

struct Node{
int k;
Node* prt;
Node* chd[];
Node(const int& key){
this->k=key;
this->prt=NULL;
this->lch=NULL;
this->rch=NULL;
}
inline int size(){
return this==NULL?:this->s;
}
inline int key(){
return this==NULL?:this->k;
}
inline int Pos(){
return this==this->prt->lch;
}
};

Node

这样定义可以直接使用new与空指针NULL而且不必在每次都判空

还可以防止delete野指针造成RE

然后是在前面我为了方便而使用的宏定义:

 #define lch chd[0]
#define rch chd[1]
#define kch chd[k]
#define xch chd[k^1]

Macro Definition

如果要维护子树大小还要记得旋转之后先维护原根(代码中的root)再维护新根(代码中的tmp)

Splay操作:

Splay(伸展)操作是Splay Tree的核心,作用是将一个指定的结点旋转到根的位置.

这时可分三种情况:

I.要伸展的结点的父节点就是根

这时直接一次单旋解决(Zig Step)

II.要伸展的结点的父节点不是根且要伸展的结点/该结点的父节点/该节点的祖父结点成一条直线

这时要先旋转祖父结点再旋转父节点且旋转方向相同(Zig-zig Step)

III.要伸展的结点的父节点不是根且要伸展的结点/该结点的父结点/该结点的祖父结点不在一条直线上

这时要先旋转父节点再反向旋转祖父结点(Zig-zag Step).需要注意的是旋转父节点后要伸展的结点的祖父结点变成了父结点.

重复上面的情况直至要伸展的结点伸展至根.

实际应用时伸展函数有时候会有两个参数:要伸展的结点指针和根的父节点指针.这样可以控制结点不一定要伸展到整棵树的根而是一个子树的根.后面Insert和Delete操作中会用到.

然后是代码实现.这里将三种情况进行了适当合并,感性理解一下就好

 void Splay(Node* root,Node* prt=NULL){
while(root->prt!=prt){
int k=root->Pos();
if(root->prt->prt==prt){
this->Rotate(root->prt,k);
}
else{
int d=root->prt->Pos();
this->Rotate(k==d?root->prt->prt:root->prt,k);
this->Rotate(root->prt,d);
}
}
}

Splay

Insert操作:

这个操作有多种写法,对于最朴素的Splay可以先按照普通二叉树的方法插入结点,然后将插入的结点伸展到根.

如果题目要求需要维护子树大小来求第K大/小的值与某数的排名的话可以用双参Splay操作与K大/排名操作配合进行,先查找该值前驱伸展到根,然后查找该值后继伸展到根的右子树,然后直接将右子树的左儿子上新建一个结点.

第一种写法的代码是我从Wikipedia上摘录的,其中将模板部分替换为了int并将下划线命名规则改为大驼峰:

 void Insert( const int &key ) {
Node *z = root;
Node *p = ; while( z ) {
p = z;
if( key<z->key ) z = z->lch;
else z = z->rch;
} z = new Node( key );
z->prt = p; if( !p ) root = z;
else if( z->key<p->key ) p->lch = z;
else p->rch = z; Splay( z );
}

Insert:写法1

第二种写法的代码如下:

 void Insert(const int& key){
int pos=this->Rank(key)-;
this->Splay(this->Kth(pos));
this->Splay(this->Kth(pos+),this->root);
Node* tmp=new Node(key);
this->root->rch->lch=tmp;
tmp->prt=this->root->rch;
this->root->rch->Maintain();
this->root->Maintain();
}

Insert: 写法2

注:Maintain()函数的作用为维护子树大小信息,Kth()为求K大函数,Rank()为求排名函数,定义见后续.

Delete操作:

Delete操作同样有多种写法,首先对于无附加信息的普通Splay:

首先查找到要删除的结点,然后伸展到根,并从它的右子树中查找值最小的结点并用它把待删除的结点替换掉.注意维护这两个结点周边结点的指针信息.代码如下,摘自Wikipedia:

 Node* Find( const T &key ) {
Node *z = root;
while( z ) {
if(key<z->key())
z=z->lch;
else if(z->key<key)
z=z->rch;
else return z;
}
return NULL;
}
void Delete( const int &key ) {
Node *z = Find( key );
if( !z ) return; Splay( z ); if( !z->lch ) Replace( z, z->rch );
else if( !z->rch ) Replace( z, z->lch );
else {
Node *y = SubtreeMinimum( z->rch );
if( y->prt != z ) {
Replace( y, y->rch );
y->rch = z->rch;
y->rch->prt = y;
}
Replace( z, y );
y->lch = z->lch;
y->lch->prt = y;
} delete z;
}
Node* subtreeMinimum( Node *u ) {
while( u->lch ) u = u->lch;
return u;
}

Delete:写法1

注:Wikipedia中使用了两个辅助函数,一个是Find()用于查找,一个是SubtreeMinimum()用于查找子树最小值.这两个函数也摘录在上面的代码中了.

对于维护了子树大小附加信息的Splay则与Insert类似,不同的是一个是新建结点一个是切断连接并删除罢了

代码如下:

 void Delete(const int& key){
int pos=this->Rank(key);
this->Splay(this->Kth(pos-));
this->Splay(this->Kth(pos+),root);
delete this->root->rch->lch;
this->root->rch->lch=NULL;
this->root->rch->Maintain();
this->root->Maintain();
}

Delete:写法2

以上即为伸展树的几种基本操作.如果我们维护了子树大小的话还可以计算第K大/小的值与某数的排名,代码如下,具体原理不再详述.

 int Rank(const int& key){
Node* root=this->root;
int rank=;
while(root!=NULL){
if(root->key()<key){
rank+=root->lch->size()+;
root=root->rch;
}
else
root=root->lch;
}
return rank;
} void Insert(const int& key){
int pos=this->Rank(key)-;
this->Splay(this->Kth(pos));
this->Splay(this->Kth(pos+),this->root);
Node* tmp=new Node(key);
this->root->rch->lch=tmp;
tmp->prt=this->root->rch;
this->root->rch->Maintain();
this->root->Maintain();
}

Kth/Rank

通过这两个函数还可求某数的前驱与后继的值,代码如下:

 inline int Predecessor(const int& key){
return this->Kth(this->Rank(key)-)->key();
} inline int Successor(const int& key){
return this->Kth(this->Rank(key+))->key();
}

Predecessor/Successor

对于Insert/Delete操作的第二种写法需要在进行所有操作前新建两个结点,值分别为INF与-INF来保证不会访问空指针

最后附上封装好的完整代码,维护了子树大小,可作为"普通平衡树"的模板.

 #define lch chd[0]
#define rch chd[1]
#define kch chd[k]
#define xch chd[k^1] const int INF=0x7FFFFFFF; class SplayTree{
private:
struct Node{
int k;
int s;
Node* prt;
Node* chd[];
Node(const int& key){
this->k=key;
this->s=;
this->prt=NULL;
this->lch=NULL;
this->rch=NULL;
}
inline int size(){
return this==NULL?:this->s;
}
inline int key(){
return this==NULL?:this->k;
}
inline void Maintain(){
if(this!=NULL)
this->s=this->lch->size()+this->rch->size()+;
}
inline int Pos(){
return this==this->prt->lch;
}
}*root; void Rotate(Node* root,int k){
Node* tmp=root->xch;
if(root->prt==NULL)
this->root=tmp;
else if(root->prt->lch==root)
root->prt->lch=tmp;
else
root->prt->rch=tmp;
tmp->prt=root->prt;
root->xch=tmp->kch;
if(root->xch!=NULL)
root->xch->prt=root;
tmp->kch=root;
root->prt=tmp;
root->Maintain();
tmp->Maintain();
} void Splay(Node* root,Node* prt=NULL){
while(root->prt!=prt){
int k=root->Pos();
if(root->prt->prt==prt){
this->Rotate(root->prt,k);
}
else{
int d=root->prt->Pos();
this->Rotate(k==d?root->prt->prt:root->prt,k);
this->Rotate(root->prt,d);
}
}
}
public:
Node* Kth(int pos){
Node* root=this->root;
while(root!=NULL){
int k=root->lch->size()+;
if(pos<k)
root=root->lch;
else if(pos==k)
return root;
else{
pos-=k;
root=root->rch;
}
}
return NULL;
} int Rank(const int& key){
Node* root=this->root;
int rank=;
while(root!=NULL){
if(root->key()<key){
rank+=root->lch->size()+;
root=root->rch;
}
else
root=root->lch;
}
return rank;
} void Insert(const int& key){
int pos=this->Rank(key)-;
this->Splay(this->Kth(pos));
this->Splay(this->Kth(pos+),this->root);
Node* tmp=new Node(key);
this->root->rch->lch=tmp;
tmp->prt=this->root->rch;
this->root->rch->Maintain();
this->root->Maintain();
} void Delete(const int& key){
int pos=this->Rank(key);
this->Splay(this->Kth(pos-));
this->Splay(this->Kth(pos+),root);
delete this->root->rch->lch;
this->root->rch->lch=NULL;
this->root->rch->Maintain();
this->root->Maintain();
} inline int Predecessor(const int& key){
return this->Kth(this->Rank(key)-)->key();
} inline int Successor(const int& key){
return this->Kth(this->Rank(key+))->key();
} SplayTree(){
this->root=new Node(-INF);
this->root->rch=new Node(INF);
this->root->rch->prt=this->root;
this->root->rch->Maintain();
this->root->Maintain();
}
};

Full Splay Tree

然后是图包时间QwQ

[学习笔记] Splay Tree 从入门到放弃的更多相关文章

  1. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  2. jQuery学习笔记 - 基础知识扫盲入门篇

    jQuery学习笔记 - 基础知识扫盲入门篇 2013-06-16 18:42 by 全新时代, 11 阅读, 0 评论, 收藏, 编辑 1.为什么要使用jQuery? 提供了强大的功能函数解决浏览器 ...

  3. Oracle RAC学习笔记:基本概念及入门

    Oracle RAC学习笔记:基本概念及入门 2010年04月19日 10:39 来源:书童的博客 作者:书童 编辑:晓熊 [技术开发 技术文章]    oracle 10g real applica ...

  4. Linux内核学习笔记-1.简介和入门

    原创文章,转载请注明:Linux内核学习笔记-1.简介和入门 By Lucio.Yang 部分内容来自:Linux Kernel Development(Third Edition),Robert L ...

  5. 【转载】【时序约束学习笔记1】Vivado入门与提高--第12讲 时序分析中的基本概念和术语

    时序分析中的基本概念和术语 Basic concept and Terminology of Timing Analysis 原文标题及网址: [时序约束学习笔记1]Vivado入门与提高--第12讲 ...

  6. 卷积神经网络(CNN)学习笔记1:基础入门

    卷积神经网络(CNN)学习笔记1:基础入门 Posted on 2016-03-01   |   In Machine Learning  |   9 Comments  |   14935  Vie ...

  7. Java IO学习笔记八:Netty入门

    作者:Grey 原文地址:Java IO学习笔记八:Netty入门 多路复用多线程方式还是有点麻烦,Netty帮我们做了封装,大大简化了编码的复杂度,接下来熟悉一下netty的基本使用. Netty+ ...

  8. React学习笔记(一)- 入门笔记

    React入门指南 作者:狐狸家的鱼 本文链接:React学习笔记 GitHub:sueRimn 1.组件内部状态state的修改 修改组件的每个状态,组件的render()方法都会再次运行.这样就可 ...

  9. nginx 学习笔记(2) nginx新手入门

    这篇手册简单介绍了nginx,并提供了一些可以操作的简单的工作.前提是nginx已经被安装到你的服务器上.如果没有安装,请阅读上篇:nginx 学习笔记(1) nginx安装.这篇手册主要内容:1. ...

随机推荐

  1. .NET 随记

    1. goto 常用于 switch语句中2. 字符串相加用 StringBuilder的Append()方法性能好3. str.Trim(',') 清除字符串后的","4. st ...

  2. Bash中的字符串变量扩展

    1.向尾部方向的最小化删除 (%) $pathname="/usr/bin/local/bin"$echo ${pathname%/bin*}/usr/bin/local 2.向尾 ...

  3. Mybatis中javaType和jdbcType对应和CRUD例子

    JDBC Type Java Type CHAR String VARCHAR String LONGVARCHAR String NUMERIC java.math.BigDecimal DECIM ...

  4. Swift3 访问权限fileprivate和 open

    在swift 3中新增加了两种访问控制权限 fileprivate和 open. 下面结合网上资料和个人理解整理一下两个属性的原理与介绍. fileprivate 在原有的swift中的 privat ...

  5. 通过ant-jmeter读取jtl文件拆分数据并insert DB

    前言:之前详解过通过jmeter生成的csv文件,解析csv存入DB,这个有弊端 第一:需独立创建一个job 第二:需按照一定规范输出 因此,放弃解析csv方式,直接解析自动化生成的原始jtl文件并集 ...

  6. Less 的使用

    Less 的使用 开发时直接使用 引用你的样式文件(main.less) (必须在less.min.js) 前引用 引用less.min.js 文件 <link href="resou ...

  7. Javascript之学习笔记每日更新

    1.输出文本 document.write(Date());输出当前时间 2.使用Jacascript改变HTML元素 //定义一个p标签,此p标签带有id元素 <p id="demo ...

  8. Mysql元数据分析

    Mysql元数据分析 @(基础技术) 一.information_schema库 information_schema库中的表,保存的是Mysql的元数据. 官网元数据表介绍 InnoDB相关的表介绍 ...

  9. win32SDK的hello,world程序

    首次用Code::Blocks写Win32GUI程序,关于GDI+的引用摸索了半天.SDK写GUI比较累人,以后还是考虑Qt或者其他方式. 代码: /** *code by lichmama from ...

  10. SpringMvc多视图配置(jsp、velocity、freemarker) 在src目录views.properties配置

    #welcome为modelAndView.setViewName(" welcome " ) ; 中的welcome .(class)固定写法 welcome.(class)=o ...