您的位置:首页 > 理论基础 > 数据结构算法

数据结构----伸展树

2016-08-02 20:50 204 查看
伸展树SPlay tree

只要理解了AVL的旋转过程,伸展树基本就能够明白的了,伸展树有个神奇之处在于把访问的节点旋转至根节点,可以实现区间删除,寻找前驱后驱等等。。。感觉伸展树比那些主席树,划分树作用更大似的,因为我能够知道用伸展树来干嘛,而其他数除了求第K大的值之外不知拿来干嘛,说到底还是做题太少,见识太少。,。需要好好熟悉理解Splay的代码。。。。。大牛都说简单,然而让我自己默写都写不出来QAQ

进入正题:

参考链接:

1点击打开链接


数据结构之伸展树

伸展树(Splay tree)学习小结

HNOI2002]营业额统计 Splay tree 

参考论文:

1)     杨思雨《伸展树的基本操作与应用》

(2)     Crash《运用伸展树解决数列维护问题》

首先要理解旋转方式:一共有六种,镜像3种,其实就是理解3种就能够明白了。(zig 右旋     zag 左旋)

x   目标结点   

y   x的父结点

z   y的父结点

1.     当y为根结点,进行一次zig或zag

2      y不是根结点,当x,y同时是各自父结点的左子树或右子树,进行两次zig或zag (可以自顶向下,也可以自底向上)

3     y不是根结点,当x,y其中一个是父结点的左子树,另一个是右子树,进行一次zag,一次zig

代码中的旋转函数比较难理解,需要会画图就容易理解了,记住需要左旋的图和右旋的图,

代码中的示例图:



① 为代码中的ch[y][!kind]=ch[x][kind];              ②pre[ch[x][kind]]=y;  (当kind=0的情况,kind=1类似)

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

1、Splay 概述

二叉查找树(Binary Search Tree,也叫二叉排序树,即Binary Sort Tree)能够支持多种动态集合操作,它可以用来表示有序集合、建立索引等,因而在实际应用中,二叉排序树是一种非常重要的数据结构。

从算法复杂度角度考虑,我们知道,作用于二叉查找树上的基本操作(如查找,插入等)的时间复杂度与树的高度成正比。对一个含n个节点的完全二叉树,这些操作的最坏情况运行时间为O(log n)。但如果因为频繁的删除和插入操作,导致树退化成一个n个节点的线性链(此时即为一个单链表),则这些操作的最坏情况运行时间为O(n)。为了克服以上缺点,很多二叉查找树的变形出现了,如红黑树、AVL树,Treap树等。

本文介绍了二叉查找树的一种改进数据结构–伸展树(Splay Tree)。它的主要特点是不会保证树一直是平衡的,但各种操作的平摊时间复杂度是O(log n),因而,从平摊复杂度上看,二叉查找树也是一种平衡二叉树。另外,相比于其他树状数据结构(如红黑树,AVL树等),伸展树的空间要求与编程复杂度要小得多。

2、 基本操作

伸展树的出发点是这样的:考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当经常处于靠近树根的位置。这样,很容易得想到以下这个方案:每次查找节点之后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操作后,它均会通过旋转的方法把被访问节点旋转到树根的位置。

为了将当前被访问节点旋转到树根,我们通常将节点自底向上旋转,直至该节点成为树根为止。“旋转”的巧妙之处就是在不打乱数列中数据大小关系(指中序遍历结果是全序的)情况下,所有基本操作的平摊复杂度仍为O(log n)。

伸展树主要有三种旋转操作,分别为单旋转,一字形旋转和之字形旋转。为了便于解释,我们假设当前被访问节点为X,X的父亲节点为Y(如果X的父亲节点存在),X的祖父节点为Z(如果X的祖父节点存在)。

(1)    单旋转

节点X的父节点Y是根节点。这时,如果X是Y的左孩子,我们进行一次右旋操作;如果X 是Y 的右孩子,则我们进行一次左旋操作。经过旋转,X成为二叉查找树T的根节点,调整结束。




(2)    一字型旋转

节点X 的父节点Y不是根节点,Y 的父节点为Z,且X与Y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次左左旋转操作或者右右旋转操作。



(3)    之字形旋转

节点X的父节点Y不是根节点,Y的父节点为Z,X与Y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次左右旋转操作或者右左旋转操作。



3、伸展树区间操作

在实际应用中,伸展树的中序遍历即为我们维护的数列,这就引出一个问题,怎么在伸展树中表示某个区间?比如我们要提取区间[a,b],那么我们将a前面一个数对应的结点转到树根,将b 后面一个结点对应的结点转到树根的右边,那么根右边的左子树就对应了区间[a,b]。原因很简单,将a 前面一个数对应的结点转到树根后, a 及a 后面的数就在根的右子树上,然后又将b后面一个结点对应的结点转到树根的右边,那么[a,b]这个区间就是下图中B所示的子树。



利用区间操作我们可以实现线段树的一些功能,比如回答对区间的询问(最大值,最小值等)。具体可以这样实现,在每个结点记录关于以这个结点为根的子树的信息,然后询问时先提取区间,再直接读取子树的相关信息。还可以对区间进行整体修改,这也要用到与线段树类似的延迟标记技术,即对于每个结点,额外记录一个或多个标记,表示以这个结点为根的子树是否被进行了某种操作,并且这种操作影响其子结点的信息值,当进行旋转和其他一些操作时相应地将标记向下传递。

与线段树相比,伸展树功能更强大,它能解决以下两个线段树不能解决的问题:

(1) 在a后面插入一些数。方法是:首先利用要插入的数构造一棵伸展树,接着,将a 转到根,并将a 后面一个数对应的结点转到根结点的右边,最后将这棵新的子树挂到根右子结点的左子结点上。

(2)  删除区间[a,b]内的数。首先提取[a,b]区间,直接删除即可。

5、 应用

(1)     数列维护问题

题目:维护一个数列,支持以下几种操作:

1. 插入:在当前数列第posi 个数字后面插入tot 个数字;若在数列首位插入,则posi 为0。

2. 删除:从当前数列第posi 个数字开始连续删除tot 个数字。

3. 修改:从当前数列第posi 个数字开始连续tot 个数字统一修改为c 。

4. 翻转:取出从当前数列第posi 个数字开始的tot 个数字,翻转后放入原来的位置。

5. 求和:计算从当前数列第posi 个数字开始连续tot 个数字的和并输出。

6. 求和最大子序列:求出当前数列中和最大的一段子序列,并输出最大和。

(2)     轻量级web服务器lighttpd中用到数据结构splay tree.

例题:

[HNOI2002]营业额统计

题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=1588

[HNOI2002]营业额统计

Time Limit: 5 Sec  Memory Limit: 162 MB
Submit: 4128  Solved: 1305

[Submit][Status][Discuss]

Description

营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况。 Tiger拿出了公司的账本,账本上记录了公司成立以来每天的营业额。分析营业情况是一项相当复杂的工作。由于节假日,大减价或者是其他情况的时候,营业额会出现一定的波动,当然一定的波动是能够接受的,但是在某些时候营业额突变得很高或是很低,这就证明公司此时的经营状况出现了问题。经济管理学上定义了一种最小波动值来衡量这种情况: 该天的最小波动值 当最小波动值越大时,就说明营业情况越不稳定。
而分析整个公司的从成立到现在营业情况是否稳定,只需要把每一天的最小波动值加起来就可以了。你的任务就是编写一个程序帮助Tiger来计算这一个值。 第一天的最小波动值为第一天的营业额。  输入输出要求

Input

第一行为正整数 ,表示该公司从成立一直到现在的天数,接下来的n行每行有一个正整数 ,表示第i天公司的营业额。

Output

输出文件仅有一个正整数,即Sigma(每天最小的波动值) 。结果小于2^31 。

Sample Input

6

5

1

2

5

4

6

Sample Output

12

HINT

结果说明:5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12

这题的数据本来有bug,少了一个数据,所有代码中用到了if(scanf("%d",&num)==EOF) num=0;
但是这题的bug改完了,所以直接scanf就行了,不过学习了一下有bug得数据的做法
#include<cstdio>
#include<iostream>
#include <algorithm>
#include <map>
#include <cmath>
using namespace std;
#define N 100005
#define inf 1<<29
int pre
,key
,ch
[2],root,tot1; //分别表示父结点,键值,左右孩子(0为左孩子,1为右孩子),根结点,结点数量
int n;
//新建一个结点
void newNode(int &r,int father,int k)
{
r=++tot1;
pre[r]=father;
key[r]=k;
ch[r][0]=ch[r][1]=0; //左右孩子为空

}
//旋转,kind为1为右旋,kind为0为左旋
/*
这里一开始很难明白,感觉很乱,但是其实这里就是旋转的过程,画图就比较容易理解了
kind一开始会有些乱,特别是kind即代表旋转方向又表示左右节点,放在一起很乱
但这就是大牛的厉害之处吧,简洁,但是熟悉明白还需要一段时间
*/
void Rotate(int x,int kind)
{
int y=pre[x];
//类似SBT,要把其中一个分支先给父节点
ch[y][!kind]=ch[x][kind];
pre[ch[x][kind]]=y;
//如果父节点不是根结点,则要和父节点的父节点连接起来
if(pre[y])
ch[pre[y]][ch[pre[y]][1]==y]=x;
pre[x]=pre[y];
ch[x][kind]=y;
pre[y]=x;
}
//Splay调整,将根为r的子树调整为goal
void Splay(int r,int goal)
{
while(pre[r]!=goal)
{
//父节点即是目标位置,goal为0表示,父节点就是根结点
if(pre[pre[r]]==goal)
Rotate(r,ch[pre[r]][0]==r);
else
{
int y=pre[r];
int kind=ch[pre[y]][0]==y;
//两个方向不同,则先左旋再右旋
if(ch[y][kind]==r)
{
Rotate(r,!kind);
Rotate(r,kind);
}
//两个方向相同,相同方向连续两次
else
{
Rotate(y,kind);
Rotate(r,kind);
}
}
}
//更新根结点
if(goal==0) root=r;
}

int Insert(int k)
{
int r=root;
while(ch[r][key[r]<k])
{
//不重复插入,这里应该是相对于这道题来讲的吧,相同值无需再插入
if(key[r]==k){
Splay(r,0);
return 0;
}
r=ch[r][key[r]<k];
}
newNode(ch[r][key[r]<k],r,k);
//将新插入的结点更新至根结点
Splay(ch[r][key[r]<k],0);
return 1;
}
//找前驱,即左子树的最右结点
int get_pre(int x)
{
int tmp=ch[x][0];
if(tmp==0) return inf;
while(ch[tmp][1])
tmp=ch[tmp][1];
return key[x]-key[tmp];
}
//找后继,即右子树的最左结点
int get_next(int x)
{
int tmp=ch[x][1];
if(tmp==0) return inf;
while(ch[tmp][0])
tmp=ch[tmp][0];
return key[tmp]-key[x];
}

int main(){

#ifndef ONLINE_JUDGE
freopen("in.txt","r",stdin);
#endif
while(~scanf("%d",&n))
{
root=tot1=0;
int ans=0;
for(int i=1;i<=n;i++)
{
int num;
//scanf("%d",&num);
//一开始数据有bug,所以这样写才能过,不过bug 改回来了
//把这里改成一般输入scanf("%d",&num)也可以过
if(scanf("%d",&num)==EOF) num=0; //读到文件结尾
if(i==1)
{
ans+=num;
newNode(root,0,num);
continue;
}
if(Insert(num)==0) continue;
int a=get_pre(root);
int b=get_next(root);
ans+=min(a,b);
}
printf("%d\n",ans);
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  伸展树Splay