认识

Splay树,BST(二叉搜索树)的一种,整体效率很高,平摊操作次数为\(O(log_2n)\),也就是说,在一棵有n个节点的BST上做M次Splay操作,时间复杂度为\(O(Mlog_2n)\)(曾经是使用最多的BST,但现在多了一个更好码的FHQ Treap),其基本操作,是把节点旋转到BST的根部,其旋转操作能很好地改善树的平衡性。

如何设计把一个节点旋转到根的方法?需要考虑以下两个目的:

(1) 每次旋转,节点x就上升一层,从而能在有限次操作后到达根部。

(2) 旋转能改善BST的平衡性(尽量使BST层数减少)。

显然,如果只考虑(1),那么使用Treap树的旋转法即可,每次x与x的父亲交换位置(x上升一层)。可Treap树的这种“单旋”并不能减少BST的层数。



于是我们要请出它的升级版:双旋。单旋不是爸爸和儿子互换吗?双旋就是把爸爸的爸爸也加进来,让儿子,爸爸,祖父三个点转着圈儿的换。一番操作下来我们就能惊奇地发现,BST的平衡性被改善了。

Splay旋转(双旋)

接下来为了方便,我们将左旋称为zig,右旋称为zag

Splay树的旋转分为两种,一字旋之字旋

(1) 一字旋:分为zig-zig和zag-zag。当x,f,g在一条直线上时,如果是向左的一条链,则做zig-zig,反之则做zag-zag。注意:应该先旋转父亲和祖父。



(2) 之字旋:也就是zig-zag,不同于一字旋,zig-zag不用先旋转父亲和祖父,可以直接旋转x,否则将不能达到减少层数的效果。

Splay树常用操作

Splay树常用于处理区间分裂和合并问题,旋转到根的功能使分裂和合并很容易实现。(作为对比,可以回顾一下FHQ Treap树的分裂与合并。)例如:一个常见的区间操作,修改或查询区间[L,R],用Splay树就很容易实现:先把L-1旋转到根,然后把节点R+1旋转到L-1的右子树上,此时,L+1的左子树就是区间[L,R]。

接下来咱们以洛谷 P4008 [NOI2003] 文本编辑器为例,说一下Splay树的常用操作。

Splay常用操作如下:

  1. 旋转

rotate(int x),对节点x做一次单旋,若x是一个右儿子,左旋,反之,右旋。

void rotate(int x){//单旋一次
int f=t[x].fa;//f:父亲
int g=t[f].fa;//g:祖父
int son=get(x);
if(son==1){//x是左儿子,右旋
t[f].rs=t[x].ls;
if(t[f].rs){
t[t[f].rs].fa=f;
}
}
else{//x是右儿子,左旋
t[f].ls=t[x].rs;
if(t[f].ls){
t[t[f].ls].fa=f;
}
}
t[f].fa=x;//x旋为f的父节点
if(son==1){//左旋,f变为x的左儿子
t[x].ls=f;
}
else{//右旋,f变为x的右儿子
t[x].rs=f;
}
t[x].fa=g;//x现在是祖父的儿子
if(g){//更新祖父的儿子
if(t[g].rs==f){
t[g].rs=x;
}
else{
t[g].ls=x;
}
}
Update(f);
Update(x);
}

Splay(int x,int goal),把节点x旋转到goal位置。goal=0表示把x旋转到根,x是新的根。\(goal\not=0\)表示把x旋转为goal的儿子。

void Splay(int x,int goal){
if(goal==0){
root=x;
}
while(1){
int f=t[x].fa;//一次处理x,f,g三个节点
int g=t[f].fa;
if(f==goal){
break;
}
if(g!=goal){//有祖父,分为一字旋和之字旋两种情况
if(get(x)==get(f)){一字旋,先旋转f,g
rotate(f);
}
else{//之字旋,直接旋转x
rotate(x);
}
}
rotate(x);
}
Update(x);
}
  1. 分裂与合并

Insert()、Del()函数中包含了分裂与合并,详情见代码注释。利用Splay函数实现分裂与合并,编码很简单。

void Insert(int L,int len){//插入一段区间
int x=kth(root,L);//x为第L个数的位置,y为第L+1个数的位置
int y=kth(root,L+1);
Splay(x,0);//分裂
Splay(y,x);
//先把x旋转到根,然后把y旋转到x的儿子,且y的儿子为空
t[y].ls=build(1,len,y);//合并:建一棵树,挂到y的左儿子上
Update(y);
Update(x);
}
void Del(int L,int R){//删除区间[L+1,R]
int x=kth(root,L);
int y=kth(root,R+1);
Splay(x,0);//y是x的右儿子,y的左儿子是待删除的区间
Splay(y,x);
t[y].ls=0;//剪短左子树,等于直接删除,这里为了简单,没有释放空间
Update(y);
Update(x);
}
  1. 块操作

每读入一个字符串,先用Build()函数把它建成一棵平衡树,然后再挂到Splay树上。而FHQ Treap树,只能一个一个地把字符添加到Treap树上,因为在FHQ Treap树中,每个节点都有一个自己的优先级,需要单独处理,不能像Splay一样对字符串做整体处理。

int build(int L,int R,int f){//把字符串建成平衡树
if(L>R){
return 0;
}
int mid=(L+R)>>1;
int cur=++cnt;
t[cur].fa=f;
t[cur].key=str[mid];
t[cur].ls=build(L,mid-1,cur);
t[cur].rs=build(mid+1,R,cur);
Update(cur);
return cur;//返回新树的根
}

Splay树能完成的操作当然远不止这些,这里只是列举了几种最常见的操作。下面就说说代码实现,还是以洛谷 P4008 [NOI2003] 文本编辑器为例。

代码实现

#include<bits/stdc++.h>//万能头文件大法好
using namespace std;
const int M=2e6+10;
int cnt=0,root=0;
struct Node{//结构体存树
int fa,ls,rs,size;//爸爸,左儿子,右儿子和大小
char key;//存的值 }t[M];
void Update(int u){//用于排名
t[u].size=t[t[u].ls].size+t[t[u].rs].size+1;
}
char str[M]={0};//输入的字符串
int build(int L,int R,int f){//把字符串建成平衡树
if(L>R){
return 0;
}
int mid=(L+R)>>1;
int cur=++cnt;
t[cur].fa=f;
t[cur].key=str[mid];
t[cur].ls=build(L,mid-1,cur);
t[cur].rs=build(mid+1,R,cur);
Update(cur);
return cur;//返回新树的根
}
int get(int x){
return t[t[x].fa].rs==x;//如果x是右儿子,返回一,反之,返回0
}
void rotate(int x){//单旋一次
int f=t[x].fa;//f:父亲
int g=t[f].fa;//g:祖父
int son=get(x);
if(son==1){//x是左儿子,右旋
t[f].rs=t[x].ls;
if(t[f].rs){
t[t[f].rs].fa=f;
}
}
else{//x是右儿子,左旋
t[f].ls=t[x].rs;
if(t[f].ls){
t[t[f].ls].fa=f;
}
}
t[f].fa=x;//x旋为f的父节点
if(son==1){//左旋,f变为x的左儿子
t[x].ls=f;
}
else{//右旋,f变为x的右儿子
t[x].rs=f;
}
t[x].fa=g;//x现在是祖父的儿子
if(g){//更新祖父的儿子
if(t[g].rs==f){
t[g].rs=x;
}
else{
t[g].ls=x;
}
}
Update(f);
Update(x);
}
void Splay(int x,int goal){
if(goal==0){
root=x;
}
while(1){
int f=t[x].fa;//一次处理x,f,g三个节点
int g=t[f].fa;
if(f==goal){
break;
}
if(g!=goal){//有祖父,分为一字旋和之字旋两种情况
if(get(x)==get(f)){一字旋,先旋转f,g
rotate(f);
}
else{//之字旋,直接旋转x
rotate(x);
}
}
rotate(x);
}
Update(x);
}
int kth(int u,int k){//第k大树的位置
if(k==t[t[u].ls].size+1){
return u;
}
if(k<=t[t[u].ls].size){
return kth(t[u].ls,k);
}
if(k>=t[t[u].ls].size+1){
return kth(t[u].rs,k-t[t[u].ls].size-1);
}
}
void Insert(int L,int len){//插入一段区间
int x=kth(root,L);//x为第L个数的位置,y为第L+1个数的位置
int y=kth(root,L+1);
Splay(x,0);//分裂
Splay(y,x);
//先把x旋转到根,然后把y旋转到x的儿子,且y的儿子为空
t[y].ls=build(1,len,y);//合并:建一棵树,挂到y的左儿子上
Update(y);
Update(x);
}
void Del(int L,int R){//删除区间[L+1,R]
int x=kth(root,L);
int y=kth(root,R+1);
Splay(x,0);//y是x的右儿子,y的左儿子是待删除的区间
Splay(y,x);
t[y].ls=0;//剪短左子树,等于直接删除,这里为了简单,没有释放空间
Update(y);
Update(x);
}
void Inorder(int u){//中序遍历
if(u==0){
return;
}
Inorder(t[u].ls);
cout<<t[u].key;
Inorder(t[u].rs);
}
int main(){
t[1].size=2;//小技巧:虚拟祖父,防止旋转时越界而出错
t[1].ls=2;
t[2].size=1;//小技巧:虚拟父亲
t[2].fa=1;
root=1,cnt=2;//在操作过程中,root将指向字符串的根
int pos=1;//光标位置
int n;
cin>>n;
while(n--){
int len;
char opt[10];
cin>>opt;
if(opt[0]=='I'){
cin>>len;
for(int i=1;i<=len;i++){
char ch=getchar();
while(ch<32||ch>126){
ch=getchar();
}
str[i]=ch;
}
Insert(pos,len);
}
if(opt[0]=='D'){
cin>>len;
Del(pos,pos+len);
}
if(opt[0]=='G'){
cin>>len;
int x=kth(root,pos);
int y=kth(root,pos+len+1);
Splay(x,0);
Splay(y,x);
Inorder(t[y].ls);
cout<<"\n";
}
if(opt[0]=='M'){
cin>>len;
pos=len+1;
}
if(opt[0]=='P'){
pos--;
}
if(opt[0]=='N'){
pos++;
}
}
return 0;//完结撒花 *\[^W^]/*
}

平衡树之Splay树详解的更多相关文章

  1. Splay树详解

    更好的阅读体验 Splay树 这是一篇宏伟的巨篇 首先介绍BST,也就是所有平衡树的开始,他的China名字是二叉查找树. BST性质简介 给定一棵二叉树,每一个节点有一个权值,命名为 ** 关键码 ...

  2. 线段树详解 (原理,实现与应用)(转载自:http://blog.csdn.net/zearot/article/details/48299459)

    原文地址:http://blog.csdn.net/zearot/article/details/48299459(如有侵权,请联系博主,立即删除.) 线段树详解    By 岩之痕 目录: 一:综述 ...

  3. B树、B+树详解

    B树.B+树详解   B树 前言 首先,为什么要总结B树.B+树的知识呢?最近在学习数据库索引调优相关知识,数据库系统普遍采用B-/+Tree作为索引结构(例如mysql的InnoDB引擎使用的B+树 ...

  4. 数据结构图文解析之:AVL树详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  5. trie字典树详解及应用

    原文链接    http://www.cnblogs.com/freewater/archive/2012/09/11/2680480.html Trie树详解及其应用   一.知识简介        ...

  6. Linux DTS(Device Tree Source)设备树详解之二(dts匹配及发挥作用的流程篇)【转】

    转自:https://blog.csdn.net/radianceblau/article/details/74722395 版权声明:本文为博主原创文章,未经博主允许不得转载.如本文对您有帮助,欢迎 ...

  7. JavaScript---Dom树详解,节点查找方式(直接(id,class,tag),间接(父子,兄弟)),节点操作(增删改查,赋值节点,替换节点,),节点属性操作(增删改查),节点文本的操作(增删改查),事件

    JavaScript---Dom树详解,节点查找方式(直接(id,class,tag),间接(父子,兄弟)),节点操作(增删改查,赋值节点,替换节点,),节点属性操作(增删改查),节点文本的操作(增删 ...

  8. Linux dts 设备树详解(二) 动手编写设备树dts

    Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 前言 硬件结构 设备树dts文件 前言 在简单了解概念之后,我们可以开始尝试写一个 ...

  9. Linux dts 设备树详解(一) 基础知识

    Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 1 前言 2 概念 2.1 什么是设备树 dts(device tree)? 2. ...

  10. AVL树详解

    AVL树 参考了:http://www.cppblog.com/cxiaojia/archive/2012/08/20/187776.html 修改了其中的错误,代码实现并亲自验证过. 平衡二叉树(B ...

随机推荐

  1. [Qt开发]当我们在开发兼容高分辨率和高缩放比、高DPI屏幕的软件时,我们在谈论什么。

    前言 最近在开发有关高分辨率屏幕的软件,还是做了不少尝试的,当然我们也去网上查了不少资料,但是网上的资料也很零碎,说不明白,这样的话我就做个简单的总结,希望看到这的你可以一次解决你有关不同分辨率下的所 ...

  2. 不好分类的好题Record

    这里装的是一些不太好分类的. problem 1 给你 \(n\) 个序列,第 \(i\) 个序列的长度为 \(m_i\),要求在每个序列中选择一个数,每种选法的代价为选择的 \(n\) 个数之和,请 ...

  3. C#.NET WINFORM 缓存 System.Runtime.Caching MemoryCache

    C#.NET WINFORM 缓存 System.Runtime.Caching MemoryCache 工具类: using System; using System.Runtime.Caching ...

  4. CentOS 7- 配置阿里镜像源

    1.备份CentOS 7系统自带yum源配置文件/etc/yum.repos.d/CentOS-Base.repo命令: mv /etc/yum.repos.d/CentOS-Base.repo /e ...

  5. monaco-editor 的 Language Services

    我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:修能 这是一段平平无奇的 SQL 语法 SELECT id ...

  6. 在线Base64转文件、文件转Base64工具

    在线Base64转换神器,一键实现Base64编码与文件互转!支持图片.文档等各类文件,快速准确,安全无服务器存储.拖拽上传,轻松编码解码,提升开发效率.跨平台兼容,移动端友好,让数据转换再无障碍. ...

  7. java8 lambda Predicate示例

    import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public class Pr ...

  8. 【ASeeker】Android 源码捞针,服务接口扫描神器

    ASeeker是一个Android源码应用系统服务接口扫描工具. 项目已开源: ☞ Github ☜ 如果您也喜欢 ASeeker,别忘了给我们点个星. 说明 ASeeker 项目是我们在做虚拟化分身 ...

  9. Nuxt 3 路由系统详解:配置与实践指南

    title: Nuxt 3 路由系统详解:配置与实践指南 date: 2024/6/21 updated: 2024/6/21 author: cmdragon excerpt: 摘要:本文是一份关于 ...

  10. Linux中的环境变量PS1,打造你的专属终端

    文章目录 介绍 PS1的格式 设置字体样式 举例 小建议 进阶 介绍 好看的终端是怎么做的呢?通过PS1这个环境变量! PS1的格式 PS1='[\u@\h \w]\$ ' 样式: 解释: [是普通字 ...