数据结构----伸展树
来源:互联网 发布:党员干部网络言行规范 编辑:程序博客网 时间:2024/06/04 01:12
伸展树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]营业额统计
[HNOI2002]营业额统计
Time Limit: 5 Sec Memory Limit: 162 MBSubmit: 4128 Solved: 1305
[Submit][Status][Discuss]
Description
Input
Output
Sample Input
5
1
2
5
4
6
Sample Output
HINT
结果说明:5+|1-5|+|2-1|+|5-5|+|4-5|+|6-5|=5+4+1+0+1+1=12
#include<cstdio>#include<iostream>#include <algorithm>#include <map>#include <cmath>using namespace std;#define N 100005#define inf 1<<29int pre[N],key[N],ch[N][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的子树调整为goalvoid 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
- 数据结构之伸展树
- 数据结构之伸展树
- 数据结构之伸展树
- 数据结构----伸展树
- 数据结构之伸展树
- 数据结构–伸展树
- 数据结构与算法-伸展树
- 数据结构:伸展树,M叉树,B树
- 无比强大的数据结构 伸展树总结
- 数据结构与算法11: 伸展树(SplayTree)
- 数据结构实现之Splay伸展树
- 数据结构Note:伸展树(Splay Tree)
- 伸展树:双层伸展
- 伸展树(SPLAY)个人总结+模板 [平衡树]【数据结构】【模板】
- 2924: 营业额统计-伸展树-Splay-数据结构-模板详解
- 数据结构之伸展树(Splay)--BST的变种
- 高级数据结构实现——自顶向下伸展树
- HDU 3308 LCIS
- 71.Coin Test
- #5.Linux的系统管理
- 72.Financial Management
- 73.The Famous Clock
- 数据结构----伸展树
- [2016/8/2]训练
- POJ 2613 Roads in the North
- 欧几里得扩展算法
- PyGobject(八十七)Gtk.PrintOperation
- 框架标签
- android中wrap_content、fill_content、match_content的用法
- poj3687 Labeling Balls
- UESTC 2016 Summer Training #18 Div.2(未完待续)