平衡树之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 ...
随机推荐
- Java中try catch finally 关键字
异常处理中的几个常用关键字(try catch finally throw throws) 异常处理 java中提供一套异常处理机制,在程序发生异常时,可以执行预先设定好的处理程序, 执行完成后,程序 ...
- DS Record
八云蓝自动机 Ⅰ 首先我们对于操作 \(1\) 转换,我们给 \(k\) 单独再开一个点 \(a_c\),这样我们就可以把操作 \(1\) 转换成操作 \(2\) 了. 对于区间问题,我们考虑使用莫队 ...
- ABC330
D 记录每一行,每一列有多少个 o,然后统计答案即可. code E 想到 \(mex^{i \le n}_{i = 1} a_i \le n\) 这整个题就可做了(赛时因为没想到这个,痛失 \(47 ...
- TiDB 多集群告警监控-中章-融合多集群 Grafana
author:longzhuquan 背景 随着公司XC改造步伐的前进,越来越多的业务选择 TiDB,由于各个业务之间需要物理隔离,避免不了的 TiDB 集群数量越来越多.虽然每套 TiDB 集群均有 ...
- leetcode | 107. 二叉树的层序遍历 II | javascript实现 | c++实现
题目 给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 . (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) 思路 题目的要求相当于是求层序遍历数组的转置,我们只需利用js的 ...
- leetcode_2-两数相加_javascript
题目 2.两数相加 给出两个 非空 的链表用来表示两个非负的整数.其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字. 如果,我们将这两个数相加起来,则会返回一个新 ...
- 第三届机器人、人工智能与信息工程国际学术会议(RAIIE 2024)
[ACM独立出版/Fellow大咖云集]2024年第二届机器人.人工智能与信息工程国际学术会议(RAIIE 2024) 2024 3rd International Symposium on Robo ...
- redis zset 延迟合并任务处理
redis zset 延迟合并任务处理 @Autowired public RedisTemplate redisTemplate; ##1.发送端:在接口中收集任务ID,累计时间段之后,合并处理. ...
- 04-Python文件操作
打开文件 f=open("我的文件.txt","r",encoding="utf8") #打开一个文件(读模式) f.close() #关闭 ...
- uniapp+thinkphp5实现微信登录
前言 之前做了微信登录,所以总结一下微信授权登录并获取用户信息这个功能的开发流程. 配置 1.首先得在微信公众平台申请一下微信小程序账号并获取到小程序的AppID和AppSecret https:// ...