热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

◆学时·VI◆SPLAY伸展树平衡树之多,学之不尽也……◇算法概述二叉排序树的一种,自动平衡,由Tarjan 提出并实现。得名于特有的Splay 操作。Splay操作:将节点u通过
◆学时·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
  3 #include
  4 #include
  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 x0: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=(valv? y->ch[0]:y->ch[1]);
 68         if(ch==NULL) break;
 69         y=ch;
 70     }
 71     Node *x=new Node(val);
 72     (valv? 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:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)

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


推荐阅读
  • 本文介绍了一种解析GRE报文长度的方法,通过分析GRE报文头中的标志位来计算报文长度。具体实现步骤包括获取GRE报文头指针、提取标志位、计算报文长度等。该方法可以帮助用户准确地获取GRE报文的长度信息。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文主要复习了数据库的一些知识点,包括环境变量设置、表之间的引用关系等。同时介绍了一些常用的数据库命令及其使用方法,如创建数据库、查看已存在的数据库、切换数据库、创建表等操作。通过本文的学习,可以加深对数据库的理解和应用能力。 ... [详细]
  • 本文介绍了一种轻巧方便的工具——集算器,通过使用集算器可以将文本日志变成结构化数据,然后可以使用SQL式查询。集算器利用集算语言的优点,将日志内容结构化为数据表结构,SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。本文还详细介绍了具体的实施过程。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • 本文介绍了在Windows系统下安装Python、setuptools、pip和virtualenv的步骤,以及安装过程中需要注意的事项。详细介绍了Python2.7.4和Python3.3.2的安装路径,以及如何使用easy_install安装setuptools。同时提醒用户在安装完setuptools后,需要继续安装pip,并注意不要将Python的目录添加到系统的环境变量中。最后,还介绍了通过下载ez_setup.py来安装setuptools的方法。 ... [详细]
  • DSP中cmd文件的命令文件组成及其作用
    本文介绍了DSP中cmd文件的命令文件的组成和作用,包括链接器配置文件的存放链接器配置信息、命令文件的组成、MEMORY和SECTIONS两个伪指令的使用、CMD分配ROM和RAM空间的目的以及MEMORY指定芯片的ROM和RAM大小和划分区间的方法。同时强调了根据不同芯片进行修改的必要性,以适应不同芯片的存储用户程序的需求。 ... [详细]
author-avatar
丶希_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有