平衡树之Splay树详解
认识
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常用操作如下:
旋转
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);
}
分裂与合并
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);
}
块操作
每读入一个字符串,先用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树详解的更多相关文章
- Splay树详解
更好的阅读体验 Splay树 这是一篇宏伟的巨篇 首先介绍BST,也就是所有平衡树的开始,他的China名字是二叉查找树. BST性质简介 给定一棵二叉树,每一个节点有一个权值,命名为 ** 关键码 ...
- 线段树详解 (原理,实现与应用)(转载自:http://blog.csdn.net/zearot/article/details/48299459)
原文地址:http://blog.csdn.net/zearot/article/details/48299459(如有侵权,请联系博主,立即删除.) 线段树详解 By 岩之痕 目录: 一:综述 ...
- B树、B+树详解
B树.B+树详解 B树 前言 首先,为什么要总结B树.B+树的知识呢?最近在学习数据库索引调优相关知识,数据库系统普遍采用B-/+Tree作为索引结构(例如mysql的InnoDB引擎使用的B+树 ...
- 数据结构图文解析之:AVL树详解及C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...
- trie字典树详解及应用
原文链接 http://www.cnblogs.com/freewater/archive/2012/09/11/2680480.html Trie树详解及其应用 一.知识简介 ...
- Linux DTS(Device Tree Source)设备树详解之二(dts匹配及发挥作用的流程篇)【转】
转自:https://blog.csdn.net/radianceblau/article/details/74722395 版权声明:本文为博主原创文章,未经博主允许不得转载.如本文对您有帮助,欢迎 ...
- JavaScript---Dom树详解,节点查找方式(直接(id,class,tag),间接(父子,兄弟)),节点操作(增删改查,赋值节点,替换节点,),节点属性操作(增删改查),节点文本的操作(增删改查),事件
JavaScript---Dom树详解,节点查找方式(直接(id,class,tag),间接(父子,兄弟)),节点操作(增删改查,赋值节点,替换节点,),节点属性操作(增删改查),节点文本的操作(增删 ...
- Linux dts 设备树详解(二) 动手编写设备树dts
Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 前言 硬件结构 设备树dts文件 前言 在简单了解概念之后,我们可以开始尝试写一个 ...
- Linux dts 设备树详解(一) 基础知识
Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 1 前言 2 概念 2.1 什么是设备树 dts(device tree)? 2. ...
- AVL树详解
AVL树 参考了:http://www.cppblog.com/cxiaojia/archive/2012/08/20/187776.html 修改了其中的错误,代码实现并亲自验证过. 平衡二叉树(B ...
随机推荐
- [Qt开发]当我们在开发兼容高分辨率和高缩放比、高DPI屏幕的软件时,我们在谈论什么。
前言 最近在开发有关高分辨率屏幕的软件,还是做了不少尝试的,当然我们也去网上查了不少资料,但是网上的资料也很零碎,说不明白,这样的话我就做个简单的总结,希望看到这的你可以一次解决你有关不同分辨率下的所 ...
- 不好分类的好题Record
这里装的是一些不太好分类的. problem 1 给你 \(n\) 个序列,第 \(i\) 个序列的长度为 \(m_i\),要求在每个序列中选择一个数,每种选法的代价为选择的 \(n\) 个数之和,请 ...
- C#.NET WINFORM 缓存 System.Runtime.Caching MemoryCache
C#.NET WINFORM 缓存 System.Runtime.Caching MemoryCache 工具类: using System; using System.Runtime.Caching ...
- CentOS 7- 配置阿里镜像源
1.备份CentOS 7系统自带yum源配置文件/etc/yum.repos.d/CentOS-Base.repo命令: mv /etc/yum.repos.d/CentOS-Base.repo /e ...
- monaco-editor 的 Language Services
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:修能 这是一段平平无奇的 SQL 语法 SELECT id ...
- 在线Base64转文件、文件转Base64工具
在线Base64转换神器,一键实现Base64编码与文件互转!支持图片.文档等各类文件,快速准确,安全无服务器存储.拖拽上传,轻松编码解码,提升开发效率.跨平台兼容,移动端友好,让数据转换再无障碍. ...
- java8 lambda Predicate示例
import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public class Pr ...
- 【ASeeker】Android 源码捞针,服务接口扫描神器
ASeeker是一个Android源码应用系统服务接口扫描工具. 项目已开源: ☞ Github ☜ 如果您也喜欢 ASeeker,别忘了给我们点个星. 说明 ASeeker 项目是我们在做虚拟化分身 ...
- Nuxt 3 路由系统详解:配置与实践指南
title: Nuxt 3 路由系统详解:配置与实践指南 date: 2024/6/21 updated: 2024/6/21 author: cmdragon excerpt: 摘要:本文是一份关于 ...
- Linux中的环境变量PS1,打造你的专属终端
文章目录 介绍 PS1的格式 设置字体样式 举例 小建议 进阶 介绍 好看的终端是怎么做的呢?通过PS1这个环境变量! PS1的格式 PS1='[\u@\h \w]\$ ' 样式: 解释: [是普通字 ...