【学时总结】◆学时·VI◆ SPLAY伸展树

◆学时·VI◆ SPLAY伸展树

平衡树之多,学之不尽也……


◇算法概述

二叉排序树的一种,自动平衡,由 Tarjan 提出并实现。得名于特有的 Splay 操作。

Splay操作:将节点u通过单旋、双旋移动到某一个指定位置。

主要目的是将访问频率高的节点在不改变原顺序的前提下移动到尽量靠近根节点的位置,以此来解决同一个(相似)问题的多次查询。

但是在非降序查询每一个节点后,Splay 树会变为一条链,降低运算效率。


◇原理&细解

(1)旋转操作

二叉排序树必须满足 左儿子<根节点<右儿子 ,即使在旋转过后也是如此。因此旋转操作(Rotate)是Splay平衡树的一个重要组成部分。而在Splay操作中,旋转分单旋和双旋。

单旋:

由于Rotate分成两种情况,许多OIer直接把两种情况分类讨论写在程序里,这样就使得Rotate()函数及其之长。但是老师教了我们一个不错的俭省代码的方法(~^o^~):

首先我们定义x的左儿子为 tree[x].ch[0],右儿子为 ch[1],再在Rotate()函数的参数表中加上"d",d=1表示右旋,0表示左旋。

void Rotate(Node *x,int d)
{
    Node *y=x->fa;
    y->ch[!d]=x->ch[d];x->fa=y->fa;
    if(x->ch[d]!=NULL) x->ch[d]->fa=y;
    if(y->fa!=NULL) y->fa->ch[y->fa->ch[1]==y]=x;
    y->fa=x;x->ch[d]=y;
    if(y==root) root=x;
    Update(y);
}

完美地契合了上图的规律,从而达到简短代码的目的!

双旋:

不用怎么解释……其实就是3个点(儿子X,父亲Y,祖父Z)之间将儿子X转移到祖父Z位置的2次旋转操作。第一次旋转能够将儿子X旋转到父亲Y位置,此时的旋转和祖父Z没有关系,就看成X,Y的旋转;第一次旋转后,Y就成了X的一棵子树,所以第二次旋转是Z和X之间的……总而言之就是两次单旋,只是注意旋转方向,保证原有关系不变。

举个例子:

reader 们可以把剩下的3种自己试一试,有什么不懂的可以在文末的邮箱处ask我 (^.^)~

(2)SPLAY操作

实质是旋转的组合……

作为Splay树的核心,它能够实现将指定节点旋转到某一个位置(或某一个节点的儿子的位置)的操作。通过Splay操作,我们可以每一次将查询的节点向高处提,从而下一次访问该节点时速度加快。

设当前需要转移的节点为x,节点y,z分别是它的父亲,祖父,x需要转移到节点rt的下方。由于每一次Rotate操作每一次可以使节点上移一层(目的一般不会是下移),如果z就是rt,就说明y是x要到达的地方(因为z的下面就是y),而x到y只需要一次Rotate,因此调用单旋。

其他情况下至少需要两次Rotate操作,即双旋。直到到达目标位置为止。

如何判断是左旋还是右旋?

我们很容易发现一个规律——如果要使V上移到U(U是V的父亲),当V是U的左儿子时,我们需要右旋,而V是U的右儿子时,需要左旋……也就是说儿子的左右和旋转方向的左右是恰好相反的。

(3)查找树中是否存在某个节点

这是所有操作中最简单的一个,只用到了二叉排序树的性质。

设查找点的值为val,从根节点开始查找,设当前查找到的点值为u。由于根的左子树小于根,而右子树大于根,所以u>val时向左子树查询,否则向右子树查询,直到查找到值或者当前节点为空NULL。

(4)插入一个特定值的节点

基本思想和查找节点很像,也是根据二叉排序树来确定位置。

当我们找到一个值恰好为特定值的节点,则将该节点的个数+1,不再插入节点了。与查找不同的是它如果按顺序查找节点,发现该节点为NULL,就说明没有值为val的节点,此时我们会新建一个值为val的节点插入到那个为NULL的节点。

(5)查询点排名以及特定排名的点

这里的排名不包括并列(2,3,3,4 3的排名为2或3,4的排名为4)。其实就是比他小(严格小于)的元素个数+1,而比他小的元素恰好就是他的左子树,因此也就是它的左子树的个数+1。

查找特定排名的点要麻烦一些……设当前节点为u,当u的左子树+1大于排名,则说明当前数过大,向左子树查询,否则向右子树查询。若查询右子树,则先将特定排名减去当前节点的左子树大小,表示在右子树中需要找到第"特定排名减去当前节点的左子树大小"大的元素。

换句话说,当前节点为u,向u的右子树查询,则目标节点在u的右子树中的排名为 (以u为根的子树中的排名 - u的左子树大小)。

(6)删除特定值的点

还是先像查找特定值的节点的思路,先找到要删除的节点的位置。由于我把值相同的点压缩在了一个点上,值相同的点的个数为cnt。当cnt>1时,即不止一个点值为特定值,我们可以直接cnt--;如果cnt=1,则删除该点后,该点就没了……这时候我们需要处理节点与其前驱后继的关系。我们可以把前驱通过Splay移动到根节点,而把后继移到前驱的右儿子。我们会发现后继的左儿子就是要删除的节点,且它没有儿子(叶结点),所以我们直接把左儿子改为NULL,再Update更新节点个数,好像就完了(=^-ω-^=)


◇手打简单模板

(PS.下面这个模板实现了插入Insert,删除Delete(无法判断是否存在该元素),查找节点GetKey,正反向查询排名Find_Count/Get_Num,查找前驱后继FrontBehind)

  1 /*Lucky_Glass*/
  2 #include<cstdio>
  3 #include<cstring>
  4 #include<algorithm>
  5 using namespace std;
  6 struct Node{
  7     Node *ch[2],*fa; //ch[0]左儿子,ch[1]右儿子
  8     int v,cnt,size; //v点权,cnt点权为v的点数量,子树大小(包括根节点)
  9     Node(int v):v(v){ //初始化
 10         fa=ch[0]=ch[1]=NULL;
 11         cnt=1;
 12     }
 13     int cmp(int x)const { //某时候极其方便的比较函数
 14         if(x==v) return -1;
 15         return x<v? 0:1;
 16     }
 17 }*root; //树根
 18 int Get_Size(Node *p) //避免点为NULL时访问size错误
 19 {
 20     return p==NULL? 0:p->size;
 21 }
 22 void Update(Node *x) //上传子树大小
 23 {
 24     x->size=1+Get_Size(x->ch[0])+Get_Size(x->ch[1]);
 25 }
 26 void Rotate(Node *x,int d) //旋转,d=0左旋,d=1右旋
 27 {
 28     Node *y=x->fa;
 29     y->ch[!d]=x->ch[d];x->fa=y->fa;
 30     if(x->ch[d]!=NULL) x->ch[d]->fa=y;
 31     if(y->fa!=NULL) y->fa->ch[y->fa->ch[1]==y]=x;
 32     y->fa=x;x->ch[d]=y;
 33     if(y==root) root=x;
 34     Update(y);
 35 }
 36 void Splay(Node *x,Node *rt)
 37 {
 38     while(x->fa!=rt) //直到到达目标位置为止
 39     {
 40         Node *y=x->fa;Node *z=y->fa;
 41         if(z==rt) //只旋转一次即到目标位置
 42             if(x==y->ch[0]) Rotate(x,1);
 43             else Rotate(x,0);
 44         else //双旋
 45             if(y==z->ch[0])
 46                 if(x==y->ch[0])
 47                     Rotate(y,1),Rotate(x,1);
 48                 else
 49                     Rotate(x,0),Rotate(x,1);
 50             else
 51                 if(x==y->ch[1])
 52                     Rotate(y,0),Rotate(x,0);
 53                 else
 54                     Rotate(x,1),Rotate(x,0);
 55     }
 56     Update(x);
 57 }
 58 void Insert(int val) //插入值为val的节点
 59 {
 60     if(root==NULL) {root=new Node(val);return;}
 61     //插入节点
 62     Node *y=root;
 63     while(true)
 64     {
 65         if(val==y->v) {y->cnt++;Splay(y,NULL);return;}
 66         //如果已经存在值为val的节点,则该节点个数+1
 67         Node *&ch=(val<y->v? y->ch[0]:y->ch[1]);
 68         if(ch==NULL) break;
 69         y=ch;
 70     }
 71     Node *x=new Node(val);
 72     (val<y->v? y->ch[0]:y->ch[1])=x;
 73     x->fa=y;
 74     Splay(x,NULL);
 75 }
 76 Node *Find(Node *x,int d) //寻找前驱后继(d=0前驱,d=1后继),只能寻找已存在于树中的值
 77 {
 78     while(x && x->ch[d]) x=x->ch[d];
 79     return x;
 80 }
 81 void Delete(int num) //删除一个值为num的节点
 82 {
 83     Node *p=root;
 84     while(true)
 85     {
 86         if(!p) return;
 87         if(p->v==num)
 88         {
 89             Splay(p,NULL);
 90             if(p->cnt==1) //单个节点
 91             {
 92                 Node *Front=Find(p->ch[0],1),
 93                      *Behind=Find(p->ch[1],0); //处理前驱后继
 94                 if(!Front && !Behind) root=NULL;
 95                 else if(!Front) root=root->ch[1],root->fa=NULL;
 96                 else if(!Behind) root=root->ch[0],root->fa=NULL;
 97                 else
 98                 {
 99                     Splay(Front,NULL);
100                     Splay(Behind,root);
101                     root->ch[1]->ch[0]=NULL;
102                     root->ch[1]->size--;
103                 }
104             }
105             else p->cnt--; //减少个数
106             return;
107         }
108         p=p->v>num? p->ch[0]:p->ch[1];
109     }
110 }
111 Node *GetKey(Node *o,int x) //根据二叉排序树关系查找值为x的节点
112 {
113     int d=o->cmp(x);
114     if(d==-1) return o;
115     return GetKey(o->ch[d],x);
116 }
117 int Find_count(int val) //找到值为val的节点在树上的排名
118 {
119     Node *x=GetKey(root,val);
120     Splay(x,NULL);
121     return Get_Size(x->ch[0])+1;
122 }
123 int Get_Num(int num) //找到排名为num的数
124 {
125     Node *now=root;
126     while(now)
127     {
128         if(num>=Get_Size(now->ch[0])+1 && num<=Get_Size(now->ch[0])+now->cnt)
129             break;
130         if(Get_Size(now->ch[0])>=num) now=now->ch[0];
131         else
132         {
133             num-=Get_Size(now->ch[0])+now->cnt;
134             now=now->ch[1];
135         }
136     }
137     Splay(now,NULL);
138     return now->v;
139 }
140 int FrontBehind(int num,int d) //找前驱后继(不一定在树上)
141 {
142     Insert(num);
143     int res=Find(root->ch[d^1],d)->v;
144     Delete(num);
145     return res;
146 }
147 int main()
148 {
149     while(true)
150     {
151         int cmd,x;
152         scanf("%d%d",&cmd,&x);
153         switch(cmd)
154         {
155             case 1: Insert(x);break;
156             case 2: Delete(x);break;
157             case 3: printf("%d\n",Find_count(x));break;
158             case 4: printf("%d\n",Get_Num(x));break;
159             case 5: printf("%d\n",FrontBehind(x,0));break;
160             case 6: printf("%d\n",FrontBehind(x,1));break;
161         }
162     }
163     return 0;
164 }

这个代码风格可能比较奇怪,因为是从几个不同的代码裁剪修改,然后组合起来的……(∩?□?∩)


The End

Thanks for reading!

- Lucky_Glass

(Tab:如果我有没讲清楚的地方可以直接在邮箱[email protected] email我,在周末我会尽量解答并完善博客~)

原文地址:https://www.cnblogs.com/LuckyGlass-blog/p/9382918.html

时间: 2024-11-12 17:03:24

【学时总结】◆学时·VI◆ SPLAY伸展树的相关文章

Splay伸展树学习笔记

Splay伸展树 有篇Splay入门必看文章 —— CSDN链接 经典引文 空间效率:O(n) 时间效率:O(log n)插入.查找.删除 创造者:Daniel Sleator 和 Robert Tarjan 优点:每次查询会调整树的结构,使被查询频率高的条目更靠近树根. Tree Rotation 树的旋转是splay的基础,对于二叉查找树来说,树的旋转不破坏查找树的结构. Splaying Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree

bzoj1208 splay伸展树

splay伸展树主要有两种操作形式 (1)正常的二叉树插入形式 功能:a.查找 b.求最大值 c.最小值 d.求前驱 e.求后继 f.删点 g.合并splay树 (这里的删除直接利用splay树的结点下标) (2)区间形式 (插入是以区间形式插入的) 区间形式的伸展树相当于线段树,支持线段树的所有操作,并且还支持区间插入这个功能, 比如操作区间[a,b],将根设为a-1,根的右孩子设为b+1,那么根的右孩子的左孩子就是所求区间 某个点插入区间也是一个道理 需要注意的是,这里init()自动生成了

Splay伸展树

伸展树,感觉对我这种菜鸟还是有些吃力,主要也是旋转的时候吧,把要查询的节点旋转到根节点,看网上是有两种方法,一是从上到下,一是从下到上.从上到下,是把树拆分成中树,左树和右树,始终保证目标节点在中树,不断向下把中树的节点添到右树或者左树,直到目标节点为中树的根,再将三树合并起来.另外一种从下到上,旋转操作类似AVL树中的旋转,但是判断好像不是很好写,我写的是从上到下的,另外删除也很巧妙,先把目标节点旋转到根,若此时左子树为空直接删除,否则找到左子树最右的节点当头,利用伸展树的特殊旋转就可以一步删

UVA 11922 Permutation Transformer —— splay伸展树

题意:根据m条指令改变排列1 2 3 4 - n ,每条指令(a, b)表示取出第a~b个元素,反转后添加到排列尾部 分析:用一个可分裂合并的序列来表示整个序列,截取一段可以用两次分裂一次合并实现,粘贴到末尾可以用一次合并实现. 翻转可以采用在每个结点上做标记的方法,flip = 1意味着将这棵子树翻转,可以类似线段树用一个pushdown()实现标记向下传递. 可以发现当前排列就是伸展树的中序遍历序列.中序遍历打印结果即可. 注意代码中设置了虚拟首结点0的技巧. 代码如下: 1 #includ

Splay伸展树入门(单点操作,区间维护)

ps:终于学会了伸展树的区间操作,做一个完整的总结,总结一下自己的伸展树的单点操作和区间维护,顺便给未来的总结复习用. splay是一种平衡树,[平均]操作复杂度O(nlogn).首先平衡树先是一颗二叉搜索树,刚刚开始学的时候找题hash数字的题先测板子... 后来那题被学长改了数据不能用平衡树测了...一道二分数字的题. 二叉搜索树的功能是,插入一个数字,在O(logn)的时间内找到它,并操作,插入删除等.但是可能会让二叉搜索树退化成链,复杂度达到O(n) 原文地址:https://www.c

codeforces 38G - Queue splay伸展树

题目 https://codeforces.com/problemset/problem/38/G 题意: 一些人按顺序进入队列,每个人有两个属性,地位$A$和能力$C$ 每个人进入时都在队尾,并最多可以和前一位互换$C$次,如果前一位的地位高于自己,则无法继续互换. 最终一次性输出整个队列 题解: splay维护动态的队列 可以用类似权值线段树的思维去考虑 对于每个点,保存其子节点的最大值,自身的值,与子树的大小 对于每次插入时,若能跨过整颗右子树与当前节点,则向左走,否则向右 可以保证整个子

splay伸展树模板

1 struct SplayTree 2 { 3 4 const static int maxn = 1e5 + 15; 5 6 int tot,root,ch[maxn][2], key[maxn], sz[maxn], lz[maxn], fa[maxn]; 7 8 void init( int x, int v = 0, int par = 0 ) 9 { 10 ch[x][0]=ch[x][1]=0, fa[x]= par, key[x] = v, sz[x] = 1; 11 } 12

HDU4453--Looploop (Splay伸展树)

Looploop XXX gets a new toy named Looploop. The toy has N elements arranged in a loop, an arrow pointing to one of the elements, and two preset parameters k1 and k2. Every element has a number on it. The figure above shows a Looploop of 6 elments. Le

树-伸展树(Splay Tree)

伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二叉查找树,即它具有和二叉查找树一样的性质:假设x为树中的任意一个结点,x节点包含关键字key,节点x的key值记为key[x].如果y是x的左子树中的一个结点,则key[y] <= key[x]:如果y是x的右子树的一个结点,则key[y] >= key[x]. (02) 除了拥有二叉查找树的性质