认识

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. ros2 foxy订阅话题问题

    代码片段 这部分代码在galactic版本编译是OK的,可在foxy下编译就出了问题 TeleopPanel::TeleopPanel(QWidget* parent) : rviz_common:: ...

  2. Centos安装Redis(极速安装)

    下载 从官网找到下载文件,我下载的是redis-6.0.16.tar.gz. 安装 1. 解压文件 解压文件然后,进入解压文件夹: tar -zxvf redis-6.0.16.tar.gz cd r ...

  3. php基本语法与安装

            // 什么是PHP         //     PHP 是 后端语言的一种          //         主要作用就是实现数据交互          //          ...

  4. kettle从入门到精通 第五十五课 ETL之kettle Excel输入

    1. Excel输入,Microsoft Excel输入步骤的作用是从Microsoft Excel中读取数据,如下图所示: 1)Excel输入步骤从文件D:\data\测试数据.xlsx读取数据. ...

  5. js获取指定日期的前一天/后一天

    date代表指定日期,格式:2018-09-27 day代表天数,-1代表前一天,1代表后一天 // date 代表指定的日期,格式:2018-09-27// day 传-1表始前一天,传1表始后一天 ...

  6. 学习ThreeJS

    创建第一个应用 使用Three JS进行编程的时候,都是在调用new Three().XXX 来实现方法,让我们先根据官方文档创建一个demo https://threejs.org/docs/ind ...

  7. CountDownLatch demo演示数据分片多线程处理

    # CountDownLatch demo演示数据分片多线程处理 package com.example.core.mydemo; import org.springframework.schedul ...

  8. 什么是浅拷贝和深拷贝,如何用 js 代码实现?

    〇.简介和对比 简介 浅拷贝:只复制原始对象的第一层属性值.   如果属性值是值类型,将直接复制值,本值和副本变更互不影响:   如果是引用数据类型,则复制内存地址,因此原始对象和新对象的属性指向相同 ...

  9. opencv在MAC下的安装

    版本信息 MAC版本:10.10.5 Xcode版本:7.2 openCV版本:2.4.13 安装步骤: 联网 安装brew,在终端输入指令 /usr/bin/ruby -e "$(curl ...

  10. qt中的 connect 函数

    1.connect()函数实现的是信号与槽的关联. 注意:只有QO bject类及其派生的类才能使用信号和槽的机制 2.函数原型 static QMetaObject::Connection conn ...