目录

  • 什么是Splay
  • 原理详解
    • 旋转操作(spin)
    • 伸展操作(splay)
  • 应用详解
    • 维护数集
      • 插入(ins)
      • 查询某权值对应的节点(find)
      • 查询某个数的前驱/后继(next)
      • 删除(del)
      • 查询第k大(rk)
      • 例题
    • 维护序列
      • 区间翻转
      • 例题

什么是Splay

对于普通的二叉查找树,每个节点的左子树里的权值均小于该节点权值,右子树里的权值均大于该节点权值。当我们向二叉查找树中添加数据时,按照上述规则就可以建立起一棵看上去还算平衡的二叉树。如果我们以3,6,5,2,1,4,73,6,5,2,1,4,73,6,5,2,1,4,7的顺序添加数据,效果如下:

事实上,对于完全随机的数据,普通的二叉查找树就可以做到O(log2n)O(log_2n)O(log2​n)的复杂度。然而出题人往往不会如此好心,通过控制添加数据的顺序,就可以让二叉查找树退化为一条链,比如按照1,2,3,4,5,6,71,2,3,4,5,6,71,2,3,4,5,6,7的顺序添加就只能得到这样的一棵“二叉查找树”:

在上述例子里,因为数据按照升序添加,所以每个节点都只有右子树,最终二叉查找树变成了一条链,让我们的操作都变成了O(n)O(n)O(n)级别的复杂度。可以看出,这个查找二叉树之所以低效,是因为整棵树“左轻右重”,如果能让整棵树长的“平衡”一些,尽量向完全二叉树靠近,就可以始终维持复杂度在O(log2n)O(log_2n)O(log2​n)级别,由此就有了“平衡树”的概念。

Splay正是一种平衡树,它的基本思想是对于“访问频繁”的节点,就把这些节点移动到靠近根的位置,以此提高整体操作的效率。我们可以认为每次访问的目标节点就是访问频繁的节点,每次操作后,就对二叉查找树进行重构,把被访问的节点搬到离根近的地方。

于是,每次都把被访问节点搬到根节点的Splay应运而生。

原理详解

通过上面的简介,可以看到实现Splay的关键在于实现“搬运”节点的操作,即将一个节点移动到根的位置同时保持查找二叉树的性质。

旋转操作(spin)

将一个节点搬运到根的第一步就是让该节点先“向上”搬运一次,使该节点的深度−1-1−1,根据当前节点vvv与其父节点fff的关系,又有“左旋”和“右旋”两种旋转方式(vvv为需要搬运的当前节点,fff为vvv的父节点,ffffff为fff的父节点):

通过以上旋转操作,节点vvv成功朝根靠近了一点,而且在旋转的同时,二叉搜索树的性质和中序dfs的遍历顺序均未发生改变。

虽然看上去有两种旋转,但在实际代码实现的时候,可以将其合并为一个函数。我们发现在一次旋转中,节点信息变化的节点有ff,f,vff,f,vff,f,v和与f,vf,vf,v的左右关系相反的vvv的子节点www(当vvv为fff右儿子的时候www是vvv的左儿子,当vvv为fff左儿子的时候www是vvv的右儿子),记录一下这些节点编号,通过少许判断就可以用一个函数实现两种旋转:

void spin(int v)
{int f=dad[v],ff=dad[f],k=son[f][1]==v,w=son[v][!k];son[ff][son[ff][1]==f]=v,dad[v]=ff;son[v][!k]=f,dad[f]=v;son[f][k]=w,dad[w]=f;up(f),up(v);//更新子树大小、子树和等信息,具体内容视题目而定
}

伸展操作(splay)

接下来就是把节点vvv旋转为节点tototo的子节点的操作了,大部分情况下都是讲vvv旋转到根节点,所以tototo一般等于000,但在某些时候(如删除节点时)也可以是其他节点,所以设置了该参数。

最简单的一个想法是,一直旋转目标节点直到成为根节点,称之为“单旋”。但在某些情况下(如一条链),仅使用单旋不能优化查找二叉树的结构,所以我们需要“双旋”操作:当ff,f,vff,f,vff,f,v的位置关系相同(即f,vf,vf,v都为自己父节点的左子树或都为自己父节点的右节点)时,先旋转fff节点,再旋转vvv节点。

可以用势能分析证明双旋Splay的时间复杂度为O(log2n)O(log_2n)O(log2​n)级别。

代码实现较易:

void splay(int v,int to)
{for(int f,ff;dad[v]!=to;spin(v)){f=dad[v],ff=dad[f];if(ff!=to)spin((son[f][0]==v)^(son[ff][0]==f)?v:f);}if(!to)root=v;//当目标为根时,记得更新根节点编号
}

应用详解

维护数集

查找二叉树的本职工作,支持插入、删除、查询数的排名、查询第kkk大、查询前驱、查询后继等操作。

用到的非记录树形结构数组有:记录节点存储的数的权值val[i]val[i]val[i],记录存储的val[i]val[i]val[i]的个数cot[i]cot[i]cot[i],记录所在子树的大小siz[i]siz[i]siz[i]

维护siz[i]siz[i]siz[i]的up函数:

void up(int v)
{siz[v]=siz[son[v][0]]+siz[son[v][1]]+cot[v];
}

插入(ins)

顺着查找二叉树左小右大的规则直接一路找下去,如果找得到对应权值的节点,直接在cot[i]cot[i]cot[i]上加一;如果找不到就新开一个节点,初始化储存信息,连接到Splay上。

连接完了记得讲新节点转到根节点,有事没事转一转维持整棵查找二叉树的平衡性是Splay复杂度的保证。

void ins(int x)
{int v=root,f=0;for(;v&&val[v]!=x;f=v,v=son[v][x>val[v]]);if(v)++cot[v];else{v=++tot,dad[v]=f,cot[v]=siz[v]=1,val[v]=x;if(f)son[f][x>val[f]]=v;}splay(v,0);
}

查询某权值对应的节点(find)

同样的,按照左小右大的规则找下去,找到了就把相应节点转到根。

代码中写的void函数,需要直到对应节点编号时直接用根节点编号即可,根据实际需要可以写成int函数返回找到的节点编号:

void find(int x)
{if(!root)return;int v=root;for(;val[v]!=x&&son[v][x>val[v]];v=son[v][x>val[v]]);splay(v,0);
}

查询某个数的前驱/后继(next)

先调用find函数,这样新的根节点一定是前驱、后继或者本身中的一个,如果新的根节点恰好满足前驱/后继的要求直接返回即可。如果与要求相反,根据左小右大的规则寻找对应节点即可。

以查找前驱为例,如果新的根节点不是前驱,那么其要么是欲查询数本身或者后继,我们要找的就是比当前根节点小的权值里最大的一个。所以我们先进入根节点的左子树,这个子树里的权值都比根节点权值小。之后,再一路沿着右儿子走,就可以找到左子树里最大的数。

找后继的情形类似,可结合代码理解。

int next(int x,int p)
{find(x);int v=root;if((val[v]<x&&!p)||(val[v]>x&&p))return v;for(v=son[v][p];son[v][!p];v=son[v][!p]);return v;
}

删除(del)

删除操作不能简单的通过find函数找到对应节点后直接删除,因为find之后若直接删去根节点,左右子树并不能快速地合并。所以要想简单直接地删去对应节点,需要让对应节点“净身出户”,即该节点不能有左右子树。

要构造这样的情形,可以调用前面编写的next函数,先将要删除的数的前驱转到根节点,再把要删除的数的后继转成根节点的右儿子。这样,根据查找二叉树的性质,后继节点的左儿子就是欲删除的节点,且该节点没有左右儿子,就可以直接删去(如果整个题目中插入的总节点数不多,可以直接将cot[i]cot[i]cot[i]设为000,这样实现更简单,缺点是Splay中的节点数可能会较多)。

void del(int x)
{int up=next(x,1),low=next(x,0);splay(low,0),splay(up,low);if(cot[son[up][0]]>1)--cot[son[up][0]],splay(son[up][0],0);else son[up][0]=0;
}

查询第k大(rk)

在VScode里起名rank的时候居然因为命名冲突编译失败了……

同样的,借助我们维护的siz[i]siz[i]siz[i]数组以及左小右大的规则,分情况讨论一下朝哪个子树走即可,结合代码非常易懂:

int rk(int x)
{if(siz[root]<x)return 0;for(int v=root;;){if(x>siz[son[v][0]]+cot[v])x-=siz[son[v][0]]+cot[v],v=son[v][1];else if(x<=siz[son[v][0]])v=son[v][0];else return v;}
}

例题

例题链接:https://www.luogu.com.cn/problem/P3369

模板题,涵盖了前面介绍的所有操作,学习和练手Splay的上上之选,这里给出完整代码:

#include<bits/stdc++.h>
using namespace std;
const int M=1e5+5;
int n,tot,root,val[M],cot[M],siz[M],son[M][2],dad[M];
void up(int v)
{siz[v]=siz[son[v][0]]+siz[son[v][1]]+cot[v];
}
void spin(int v)
{int f=dad[v],ff=dad[f],k=son[f][1]==v,w=son[v][!k];son[ff][son[ff][1]==f]=v,dad[v]=ff;son[v][!k]=f,dad[f]=v;son[f][k]=w,dad[w]=f;up(f),up(v);
}
void splay(int v,int to)
{for(int f,ff;dad[v]!=to;spin(v)){f=dad[v],ff=dad[f];if(ff!=to)spin((son[f][0]==v)^(son[ff][0]==f)?v:f);}if(!to)root=v;
}
void ins(int x)
{int v=root,f=0;for(;v&&val[v]!=x;f=v,v=son[v][x>val[v]]);if(v)++cot[v];else{v=++tot,dad[v]=f,cot[v]=siz[v]=1,val[v]=x;if(f)son[f][x>val[f]]=v;}splay(v,0);
}
void find(int x)
{if(!root)return;int v=root;for(;val[v]!=x&&son[v][x>val[v]];v=son[v][x>val[v]]);splay(v,0);
}
int next(int x,int p)
{find(x);int v=root;if((val[v]<x&&!p)||(val[v]>x&&p))return v;for(v=son[v][p];son[v][!p];v=son[v][!p]);return v;
}
void del(int x)
{int up=next(x,1),low=next(x,0);splay(low,0),splay(up,low);if(cot[son[up][0]]>1)--cot[son[up][0]],splay(son[up][0],0);else son[up][0]=0;
}
int rk(int x)
{if(siz[root]<x)return 0;for(int v=root;;){if(x>siz[son[v][0]]+cot[v])x-=siz[son[v][0]]+cot[v],v=son[v][1];else if(x<=siz[son[v][0]])v=son[v][0];else return v;}
}
void in(){scanf("%d",&n);}
void ac()
{ins(-INT_MAX),ins(INT_MAX);for(int i=1,a,b;i<=n;++i){scanf("%d%d",&a,&b);if(a==1)ins(b);else if(a==2)del(b);else if(a==3)find(b),printf("%d\n",siz[son[root][0]]);else if(a==4)printf("%d\n",val[rk(b+1)]);else if(a==5)printf("%d\n",val[next(b,0)]);else printf("%d\n",val[next(b,1)]);}
}
int main()
{in(),ac();system("pause");
}

维护序列

因为Splay不会改变中序遍历的遍历顺序,我们把节点权值设置为序列里的编号就可以用Splay维护一个序列。

跟线段树维护序列类似,Splay同样可以通过打标记、合并左右子树信息来完成线段树支持的区间修改,但是如果线段树都能完成维护,为什么要用Splay呢?

区间翻转

因为从Splay还原到序列是通过中序遍历,所以对于序列里的一个区间进行翻转就相当于把该区间对应的子树里每个节点的左右儿子都交换一次,这是线段树无法实现的功能,而Splay用打标记的方式可以轻松实现。

带标记下放的Splay就是在spaly函数里多加上push函数就行了,记得从上到下依次下放。

例题

例题链接:https://www.luogu.org/problemnew/show/P3165

博主之前水水的题解

#include<bits/stdc++.h>
using namespace std;
int root,tot,f,ff;
bool k;
struct node{int son[2],dad,fir,rk,sz;bool rev;node(){memset(this,0,sizeof(this));}
};
struct sd{int n,fir;bool operator < (const sd &x) const{if(n!=x.n) return n<x.n;return fir<x.fir;}
};
sd x[100005];
node tree[100005];
void up(int v)
{tree[v].sz=tree[tree[v].son[0]].sz+tree[tree[v].son[1]].sz+1;
}
void push(int v)
{if(tree[v].rev){tree[tree[v].son[0]].rev^=1;tree[tree[v].son[1]].rev^=1;swap(tree[v].son[0],tree[v].son[1]);tree[v].rev=0;}
}
void spin(int v)
{f=tree[v].dad;ff=tree[f].dad;k=(tree[f].son[1]==v);tree[ff].son[tree[ff].son[1]==f]=v;tree[v].dad=ff;tree[f].son[k]=tree[v].son[!k];tree[tree[v].son[!k]].dad=f;tree[v].son[!k]=f;tree[f].dad=v;up(f);up(v);
}
void splay(int v,int goal)
{while(tree[v].dad!=goal){f=tree[v].dad;ff=tree[f].dad;if(ff)push(ff);if(f)push(f);if(v)push(v);if(ff!=goal)((tree[ff].son[0]==f)^(tree[f].son[0]==v))?spin(v):spin(f);spin(v);}if(!goal) root=v;
}
void insert(int fir,int rk)
{int v=root;f=0;while(tree[v].fir!=fir&&v)f=v,v=tree[v].son[fir>tree[v].fir];v=++tot;if(f) tree[f].son[fir>tree[f].fir]=v;tree[v].sz=1;tree[v].fir=fir;tree[v].rk=rk;tree[v].dad=f;splay(v,0);
}
int s;
int rank(int x)
{int v=root;while(1){push(v);s=tree[v].son[0];if(x>tree[s].sz+1){x-=tree[s].sz+1;v=tree[v].son[1];}elseif(tree[s].sz>=x) v=s;else return v;}
}
int n;
void in()
{scanf("%d",&n);for(int i=1;i<=n;++i)scanf("%d",&x[i].n),x[i].fir=i;sort(x+1,x+n+1);for(int i=1;i<=n;++i)insert(x[i].fir,i);insert(-1,-1);insert(n+1,1e9+5);
}
void ac()
{int le,ri,hh;splay(1,0);hh=tree[tree[root].son[0]].sz;printf("%d",hh);le=rank(1);ri=rank(hh+2);splay(le,0);splay(ri,le);tree[tree[ri].son[0]].rev^=1;for(int i=2;i<=n;++i){splay(i,0);hh=tree[tree[root].son[0]].sz;printf(" %d",hh);if(i==n) return;ri=rank(hh+2);le=i-1;splay(le,0);splay(ri,le);tree[tree[ri].son[0]].rev^=1;}
}
int main()
{in();ac();return 0;
}

[数据结构] 伸展树(Splay Tree)原理及若干应用详解(无指针)相关推荐

  1. splay tree java_伸展树(splay tree)自顶向下的算法

    伸展树(splay tree)是一种能自我调整的二叉搜索树(BST).虽然某一次的访问操作所花费的时间比较长,但是平摊(amortized) 之后的访问操作(例如旋转)时间能达到O(logn)的复杂度 ...

  2. 伸展树(Splay tree)图解与实现

    伸展树(Splay tree)图解与实现 伸展树(Splay tree)图解与实现_小张的专栏-CSDN博客_splay树 Splay树详解 Splay树详解 - 秦淮岸灯火阑珊 - 博客园 平衡树 ...

  3. 伸展树(Splay tree)浅谈

    树看的越来越多,越来越神奇. 看伸展树这种神级数据结构之前,建议大家首先彻底明白二叉搜索树,这是万树的基础. 然后可以去看下treap,最好再去看下红黑树.如果有线段树的基础那更好了,我们会发现线段树 ...

  4. Btree/B+tree原理及区别(详解)

    1,B-tree 什么是B-tree B-tree是一种多路自平衡搜索树,它类似普通的二叉树,但是Btree允许每个节点有更多的子节点.Btree示意图如下: 由上图可知 B-tree 的一些特点: ...

  5. 数据结构--伸展树(伸展树构建二叉搜索树)-学习笔记

    2019/7/16更新:封装SplayTree进入class:例题:http://poj.org/problem?id=3622 一个伸展树的板子: #include<stdio.h> # ...

  6. 行为树 Behavior Tree 原理

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接: https://blog.csdn.net/LIQIANGEASTSUN/arti ...

  7. 行为树 Behavior Tree 原理 一

    行为树 Behavior Tree 原理 一 行为树 结构图如下,一棵倒置的树 行为树采用节点描述行为逻辑,主要节点类型有: 组合节点:选择节点.顺序节点.随机选择节点.随机顺序节点.随机权重节点.并 ...

  8. Lesson 8.3Lesson 8.4 ID3、C4.5决策树的建模流程CART回归树的建模流程与sklearn参数详解

    Lesson 8.3 ID3.C4.5决策树的建模流程 ID3和C4.5作为的经典决策树算法,尽管无法通过sklearn来进行建模,但其基本原理仍然值得讨论与学习.接下来我们详细介绍关于ID3和C4. ...

  9. java同步方法完成案例_Java同步代码块和同步方法原理与应用案例详解

    本文实例讲述了java同步代码块和同步方法.分享给大家供大家参考,具体如下: 一 点睛 所谓原子性WOmoad:一段代码要么执行,要么不执行,不存在执行一部分被中断的情况.言外之意是这段代码就像原子一 ...

  10. python创建双链表_Python双链表原理与实现方法详解

    本文实例讲述了Python双链表原理与实现方法.分享给大家供大家参考,具体如下: Python实现双链表 文章目录 Python实现双链表 单链表与双链表比较 双链表的实现 定义链表节点 初始化双链表 ...

最新文章

  1. 听说你 ping 用的很 6 ?给我图解一下 ping 的工作原理!
  2. 深入浅出经典面试题:从浏览器中输入URL到页面加载发生了什么 - Part 3
  3. Axure--Web原型开发工具
  4. C++求tree树的高度(附完整源码)
  5. 和功率的计算公式_电机功率计算公式是什么?
  6. 学习响应式BootStrap来写融职教育网站,Bootsrtap第九天手粉琴swiper特效
  7. redis连接与redis的python连接
  8. numpy 平方_NumPy入门指南
  9. Win7系统防火墙设置不了怎么办
  10. Mysql经常使用命令
  11. JavaScript学习(二十九)—JS常用的事件
  12. 人生苦短快用python_人生苦短,快用 Python
  13. 生成模型和判别模型直接的区别
  14. Java中的堆栈API——Stack
  15. 正版Oracle产品价格
  16. STM32CUBE 定时器使用
  17. php mysql某小型汽车维修店信息管理系统zjyY3
  18. 程序员用300行代码,让外婆实现语音搜索购物
  19. 安装 FME Desktop 2020 教程(内置补丁可以有效激活软件)
  20. 《版式设计——日本平面设计师参考手册》—第1章段落格式的设置

热门文章

  1. 利用Outlook应用程序接口执行Shellcode
  2. Fudan-NLP-Beginner:自然语言处理入门练习
  3. matlab里日期函数,matlab中如何获取当前日期时间函数的具体应用如下
  4. IDEA中将WEB-INF\lib下的Jar包添加到项目中
  5. python测试开发django-46.xadmin添加action动作
  6. 更改input标签的placeholder的样式
  7. 创新课程管理系统——测试心得
  8. Phoenix Tips (8) 多租户
  9. 又延伸到socket去了。
  10. Spring源码分析-从@ComponentScan注解配置包扫描路径到IoC容器中的BeanDefinition,经历了什么(一)?