Splay Tree(伸展树)

article/2025/11/9 15:48:52

二叉查找树(Binary Search Tree)能够支持多种动态集合操作。因此,在信息学竞赛中,二叉排序树起着非常重要的作用,它可以被用来表示有序集合、建立索引或优先队列等。

作用于二叉查找树上的基本操作的时间是与树的高度成正比的。对一个含n各节点的完全二叉树,这些操作的最坏情况运行时间为O(log n)。但如果树是含n个节点的线性链,则这些操作的最坏情况运行时间为O(n)。而有些二叉查找树的变形,其基本操作在最坏情况下性能依然很好,比如红黑树、AVL树等等。

本文将要介绍的伸展树(Splay Tree),也是对二叉查找树的一种改进,虽然它并不能保证树一直是“平衡”的,但对于伸展树的一系列操作,我们可以证明其每一步操作的平摊复杂度都是O(log n)。所以从某种意义上说,伸展树也是一种平衡的二叉查找树。而在各种树状数据结构中,伸展树的空间要求与编程复杂度也都是很优秀的。

 

【伸展树的基本操作】

伸展树是二叉查找树的一种改进,与二叉查找树一样,伸展树也具有有序性。即伸展树中的每一个节点x都满足:该节点左子树中的每一个元素都小于x,而其右子树中的每一个元素都大于x。与普通二叉查找树不同的是,伸展树可以自我调整,这就要依靠伸展操作Splay(x,S)。

伸展操作 Splay(x,S)

伸展操作Splay(x,S)是在保持伸展树有序性的前提下,通过一系列旋转将伸展树S中的元素x调整至树的根部。在调整的过程中,要分以下三种情况分别处理:

情况一:节点x的父节点y是根节点。这时,如果x是y的左孩子,我们进行一次Zig(右旋)操作;如果x 是y 的右孩子,则我们进行一次Zag(左旋)操作。经过旋转,x成为二叉查找树S的根节点,调整结束。即:如果当前结点父结点即为根结点,那么我们只需要进行一次简单旋转即可完成任务,我们称这种旋转为单旋转。如图1所示

 

 

(图1)

情况二:节点x 的父节点y 不是根节点,y 的父节点为z,且x 与y 同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次Zig-Zig操作或者Zag-Zag操作。即:设当前结点为X , X 的父结点为Y ,Y 的父结点为Z ,如果Y 和X 同为其父亲的左孩子或右孩子,那么我们先旋转Y ,再旋转X 。我们称这种旋转为一字形旋转。如图2所示

 

(图2)

情况三:节点x的父节点y不是根节点,y的父节点为z,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次Zig-Zag操作或者Zag-Zig 操作。即:这时我们连续旋转两次X 。我们称这种旋转为之字形旋转。如图3所示

 

(图3)

如图4所示,执行Splay(1,S),我们将元素1 调整到了伸展树S 的根部。再执行Splay(2,S),如图5 所示,我们从直观上可以看出在经过调整后,伸展树比原来“平衡”了许多。而伸展操作的过程并不复杂,只需要根据情况进行旋转就可以了,而三种旋转都是由基本得左旋和右旋组成的,实现较为简单。

 

(图4)

(图5)

 

利用Splay操作,我们可以在伸展树S上进行如下运算:

(1)Find(x,S):判断元素x是否在伸展树S表示的有序集中。

首先,与在二叉查找树中的查找操作一样,在伸展树中查找元素x。如果x在树中,则再执行Splay(x,S)调整伸展树。

(2)Insert(x,S):将元素x插入伸展树S表示的有序集中。

首先,也与处理普通的二叉查找树一样,将x 插入到伸展树S中的相应位置上,再执行Splay(x,S)。

(3)Delete(x,S):将元素x从伸展树S所表示的有序集中删除。

首先,用在二叉查找树中查找元素的方法找到x的位置。如果x没有孩子或只有一个孩子,那么直接将x删去,并通过Splay操作,将x节点的父节点调整

到伸展树的根节点处。否则,则向下查找x的后继y,用y替代x的位置,最后执行Splay(y,S),将y调整为伸展树的根。

(4)Join(S1,S2):将两个伸展树S1与S2合并成为一个伸展树。其中S1的所有元素都小于S2的所有元素。首先,我们找到伸展树S1 中最大的一个元素x,再通过Splay(x,S1)将x 调整到伸展树S1 的根。然后再将S2 作为x 节点的右子树。这样,就得到了新的伸展树S。如图6所示

 

(图6)

(5)Split(x,S):以x 为界,将伸展树S 分离为两棵伸展树S1 和S2,其中S1中所有元素都小于x,S2中的所有元素都大于x。首先执行Find(x,S),将元素x 调整为伸展树的根节点,则x 的左子树就是S1,而右子树为S2。如图7所示

 

(图7)

除了上面介绍的五种基本操作,伸展树还支持求最大值、求最小值、求前趋、求后继等多种操作,这些基本操作也都是建立在伸展操作的基础上的。

 

通常来说,每进行一种操作后都会进行一次Splay 操作,这样可以保证每次操作的平摊时间复杂度是O(log n)。关于证明可以参见相关书籍和论文。

既然可以把任何一个结点转到根,那么也就可以把任意一个结点转到其到根路径上任何一个结点的下面(特别地,转到根就是转到空结点Null 的下面)。下面的利用伸展树维护数列就要用到将一个结点转到某个结点下面。

最后附上Splay 操作的代码:

 

// node 为结点类型,其中ch[0]表示左结点指针,ch[1]表示右结点指针
// pre 表示指向父亲的指针
void Rotate(node *x, int c) // 旋转操作,c=0 表示左旋,c=1 表示右旋
{node *y = x->pre;y->ch[! c] = x->ch[c];if (x->ch[c] != Null) x->ch[c]->pre = y;x->pre = y->pre;if (y->pre != Null)if (y->pre->ch[0] == y) y->pre->ch[0] = x;else y->pre->ch[1] = x;x->ch[c] = y, y->pre = x;if (y == root) root = x; // root 表示整棵树的根结点
}
void Splay(node *x, node *f) // Splay 操作,表示把结点x 转到结点f 的下面
{for ( ; x->pre != f; )if (x->pre->pre == f) // 父结点的父亲即为f,执行单旋转if (x->pre->ch[0] == x) Rotate(x, 1);else Rotate(x, 0);else{node *y = x->pre, *z = y->pre;if (z->ch[0] == y)if (y->ch[0] == x)Rotate(y, 1), Rotate(x, 1); // 一字形旋转elseRotate(x, 0), Rotate(x, 1); // 之字形旋转else if (y->ch[1] == x)Rotate(y, 0), Rotate(x, 0); // 一字形旋转elseRotate(x, 1), Rotate(x, 0); // 之字形旋转}
}

 

 

 

 

 

 

 

【伸展树的区间操作】

首先我们认为伸展树的中序遍历即为我们维护的数列,那么很重要的一个操作就是怎么在伸展树中表示任意一个区间。比如我们要提取区间a,b],那么我们将a前面一个数对应的结点转到树根,将b 后面一个结点对应的结点转到树根的右边,那么根右边的左子树就对应了区间[a,b]。其中的道理也是很简单的,将a 前面一个数对应的结点转到树根后, a 及a 后面的数就在根的右子树上,然后又将b后面一个结点对应的结点转到树根的右边,那么[a,b]这个区间就是图8中*所示的子树。

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

 

(图8)

到目前为止,伸展树只是实现了线段树能够实现的功能,下面两个功能将是线段树无法办到的。如果我们要在a 后面插入一些数,那么我们先把这些插入的数建成一棵伸展树,我们可以利用分治法建立一棵完全平衡的二叉树,就是说每次把最中间的作为当前区间的根,然后左右递归处理,返回的时候进行维护。接着将a 转到根,将a 后面一个数对应的结点转到根结点的右边,最后将这棵新的子树挂到根右子结点的左子结点上。还有一个操作就是删除一个区间[a,b]内的数,像上面一样,我们先提取区间,然后直接删除那棵子树,即可达到目的。最后还需注意的就是,每当进行一个对数列进行修改的操作后,都要维护伸展树,一种方法就是对影响到的结点从下往上执行Update 操作。但还有一种方法,就是将修改的结点旋转到根,因为Splay 操作在旋转的同时也会维护每个结点的值,因此可以达到对整个伸展树维护的目的。最后还有一个小问题,因为数列中第一个数前面没有数字了,并且最后一个数后面也没有数字了,这样提取区间时就会出一些问题。为了不进行过多的特殊判断,我们在原数列最前面和最后面分别加上一个数,在伸展树中就体现为结点,这样提取区间的时候原来的第k个数就是现在的第k +1个数。并且我们还要注意,这两个结点维护的信息不能影响到正确的结果。下面看一下新的Splay 操作的程序(能对结点信息进行维护):

 

// node 为结点类型,其中ch[0]表示左结点指针,ch[1]表示右结点指针
// pre 表示指向父亲的指针
void Rotate(node *x, int c) // 旋转操作,c=0 表示左旋,c=1 表示右旋
{node *y = x->pre;Push_Down(y), Push_Down(x);
// 先将Y 结点的标记向下传递(因为Y 在上面),再把X 的标记向下传递y->ch[! c] = x->ch[c];if (x->ch[c] != Null) x->ch[c]->pre = y;x->pre = y->pre;if (y->pre != Null)if (y->pre->ch[0] == y) y->pre->ch[0] = x;else y->pre->ch[1] = x;x->ch[c] = y, y->pre = x, Update(y); // 维护Y 结点if (y == root) root = x; // root 表示整棵树的根结点
}
void Splay(node *x, node *f) // Splay 操作,表示把结点x 转到结点f 的下面
{for (Push_Down(x) ; x->pre != f; ) // 一开始就将X 的标记下传if (x->pre->pre == f) // 父结点的父亲即为f,执行单旋转if (x->pre->ch[0] == x) Rotate(x, 1);else Rotate(x, 0);else{node *y = x->pre, *z = y->pre;if (z->ch[0] == y)if (y->ch[0] == x)Rotate(y, 1), Rotate(x, 1); // 一字形旋转elseRotate(x, 0), Rotate(x, 1); // 之字形旋转else if (y->ch[1] == x)Rotate(y, 0), Rotate(x, 0); // 一字形旋转elseRotate(x, 1), Rotate(x, 0); // 之字形旋转}Update(x); // 最后再维护X 结点
}

 

 

 

 

 

可能有人会问,为什么在旋转的时候只对X 结点的父亲进行维护,而不对X结点进行维护,但是Splay 操作的最后却又维护了X 结点?原因很简单。因为除了一字形旋转,在Splay 操作里我们进行的旋转都只对X 结点进行,因此过早地维护是多余的;而在一字形旋转中,好像在旋转中没有对X 的父亲进行维护,但后面紧接着就是旋转X 结点,又会对X 的父亲进行维护,也是没问题的。这样可以节省不少冗余的Update 操作,能减小程序隐含的常数。

最后我们看看怎么样实现把数列中第k 个数对应的结点转到想要的位置。对于这个操作,我们要记录每个以结点为根子树的大小,即包含结点的个数,然后从根开始,每次决定是向左走,还是向右走,具体见下面的代码:

 

// 找到处在中序遍历第k 个结点,并将其旋转到结点f 的下面
void Select(int k, node *f)
{int tmp;node *t;for (t = root; ; ) // 从根结点开始{Push_Down(t); // 由于要访问t 的子结点,将标记下传tmp = t->ch[0]->size; // 得到t 左子树的大小if (k == tmp + 1) break; // 得出t 即为查找结点,退出循环if (k <= tmp) // 第k 个结点在t 左边,向左走t = t->ch[0];else // 否则在右边,而且在右子树中,这个结点不再是第k 个k -= tmp + 1, t = t->ch[1];}Splay(t, f); // 执行旋转
}

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

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

 

 

 


http://chatgpt.dhexx.cn/article/BXBPPMov.shtml

相关文章

浅谈splay

例题&#xff1a; 给出一个长度为n序列a。 有m次操作&#xff0c;每次操作可以修改a[i]&#xff0c;在第i个数前插入一个数x&#xff0c;或查询区间[l,r]的最大值。 1≤n≤100000,1≤m≤100000。 强制在线 看到这道题最自然的反应就是用线段树&#xff0c;但有了插入操作线段树…

关于splay的一些说明

前言splay出现的背景它的操作 push_uprotatesplayfindinsertnextdeletefindkthmain 前言 暑假快过完了,大家感觉是不是很棒!gay房也体验过最新的断电模拟器了. 好吧,在开讲之前我还是得说一个.刀剑神域的外传实在太好看了! 除了精彩的战斗场面,神一般的人设和强大的剧情,比…

Splay入门详解

Splay入门详解 写在前面 听说平衡树是一种强大的数据结构&#xff0c;听同年级或高年级大佬们讲起来也感觉很牛笔的亚子&#xff0c;而最近XC又叫我们去学习一下 L C T LCT LCT&#xff01;&#xff1f; 又因为 S p l a y Splay Splay是学习 L C T LCT LCT的基础&#xff0c;…

【学习笔记】Splay

普通平衡树 模板题链接 1、引入 一种二叉树&#xff0c;这棵树满足任意一个节点&#xff0c;它的左儿子的权值<自己的权值<右儿子的权值 这种树叫做二叉查找树&#xff0c;这个概念应该在初赛中见过了吧 Splay就是利用这个结构来实现的 2、变量 模板题的7大变量 s…

平衡树详解(Splay)

引入 先看例题&#xff1a;&#xff08;洛谷 P3369 【模板】普通平衡树&#xff09; 您需要写一种数据结构&#xff0c;来维护一些数&#xff0c;其中需要提供以下操作&#xff1a; 1.插入 x x x 数 2.删除 x x x 数(若有多个相同的数&#xff0c;因只删除一个) 3.查询 x x…

Splay入门解析【保证让你看不懂(滑稽)】

upd&#xff1a;忽然发现直接在百度上搜索 splay s p l a y 就会搜索到我这篇文章&#xff1f;小蒟蒻瑟瑟发抖。 因为CSDN这边我基本不会看回复什么的&#xff0c;所以如果有问题还是请移步cnblogs里面吧 Cnblogs里面这篇文章的链接 BST真是神奇的东西。。。 而且种类好多呀…

Splay Tree

类别&#xff1a;二叉排序树 空间效率&#xff1a;O(n) 时间效率&#xff1a;O(log n)内完成插入、查找、删除操作 创造者&#xff1a;Daniel Sleator和Robert Tarjan 优点&#xff1a;每次查询会调整树的结构&#xff0c;使被查询频率高的条目更靠近树根。 注&#xff1a;所有…

【算法详解】splay的初步了解

qwq表示最近感受到了不停课的鸭梨啊&#xff0c;好难搞啊……算法太难学了……我好菜啊qwq 其实半个多月前就可以写这篇文章&#xff0c;不过由于时间紧张没写emmmmmm&#xff0c;不扯闲言乱语了&#xff0c;如果有什么写的不好的地方凑活一下吧2333333333 splay是一种可以使树…

splay详解

一&#xff1a;什么是splay&#xff08;伸展树&#xff09; 首先了解二叉排序树 二叉排序树或者是一棵空树&#xff0c;或者是具有下列性质的 二叉树&#xff1a; &#xff08;1&#xff09;若左子树不空&#xff0c;则左子树上所有结点的值均小于或等于它的 根结点的值&#…

Splay 总结基础精华

前言&#xff1a; 第一次学习Splay是2月份&#xff0c;打板子的时候是3月份&#xff0c;Ac是4月份&#xff0c;写这篇博客是6月初&#xff1b;原因是因为我竟然发现我忘了Splay的板子了&#xff01;很慌&#xff0c;必须总结一下&#xff01; 不敢说是最详细的&#xff0c;但希…

关系型数据库与非关系型数据库详解

关系数据库与非关系型数据库 一、数据库概述1、关系型数据库2、非关系型数据库 二、数据库区别1、数据存储方式不同2、扩展方式不同3、对事务性的支持不同 三、非关系型数据库产生背景四、Redis简介1、Redis 优点 五、Redis 安装部署六、Redis 命令工具&#xff08;1&#xff0…

关系型数据库RDS基本简介

OSS存放非结构化的数据&#xff0c;如&#xff1a;音频、视频、图片 rds存放结构化的数据&#xff0c;如&#xff1a;一张表&#xff0c; RDS产品概要 可以根据业务的需求进行弹性伸缩。 采用主从备份架构&#xff0c;三节点数据存储&#xff0c;具备高可用性和数据可靠性 …

2 关系型数据库是什么?

目录结构 关系型数据库基本概念结构化查询语言数据定义语言&#xff08;DDL&#xff09;数据查询语言&#xff08;Data Query Language, DQL&#xff09;数据操纵语言&#xff08;Data Manipulation Language, DML&#xff09;数据控制语言&#xff08;Data Control Language, …

常见的关系型数据库有哪些

Oracle Database&#xff1a;由Oracle公司开发和维护的商业关系型数据库&#xff0c;具有广泛的应用场景和功能。 MySQL&#xff1a;开源关系型数据库&#xff0c;常用于Web应用程序和小型企业应用。 Microsoft SQL Server&#xff1a;由Microsoft公司开发和维护的商业关系型…

Bezier曲线快速相交计算(含代码)

Bezier曲线快速相交计算 背景介绍算法思路解释和分析示例参考资料 背景介绍 很多时候&#xff0c;需要计算曲线段与曲线段是否有交点。常规的思路是直接联立方程求解。不过&#xff0c;直接求方程的解这种思路通常在计算上开销较大。 针对任意曲线&#xff0c;曲线的方程阶次…

js绘制贝塞尔曲线(Bézier-Curve)

贝塞尔曲线(Bzier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线&#xff0c;贝兹曲线由线段与节点组成&#xff0c;节点是可拖动的支点&#xff0c;线段像可伸缩的皮筋&#xff0c;我们…

Bezier曲线与B-Spline曲线

微分几何基础 微分集合是用微分的方法来研究曲线的局部性质&#xff0c;如曲线的弯曲程度等。 一条可以用参数表示的三维曲线是一个有界点集&#xff0c;可以写成一个带参数的、连续的、单值的数学函数&#xff1a; { x x ( t ) y y ( t ) z z ( t ) 0 ⩽ t ⩽ 1 p p ( t…

初识贝塞尔(bezier)曲线

文章目录 资料援引贝塞尔曲线的用途一阶贝塞尔&#xff08;bezier&#xff09;曲线二阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔&#xff08;bezier&#xff09;曲线高阶贝塞尔&#xff08;bezier&#xff09;曲线三阶贝塞尔曲线求插值&#xff08;Slerp&#xff0…

贝塞尔曲线(Bezier Curve)原理、公式推导及matlab代码实现

1. 定义 贝塞尔曲线(Bezier curve)&#xff0c;又称贝兹曲线或贝济埃曲线&#xff0c;是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线&#xff0c;贝兹曲线由线段与节点组成&#xff0c;节点是可拖动的支点&#xff0c;线段像可伸缩的皮筋&#…

Bezier(贝塞尔曲线通用规律算法-DEMO)

之前也看过一些相关贝塞尔曲线的知识&#xff0c;但就是一直没有实践应用&#xff1b; 今天&#xff0c;听到有同事&#xff1a;程序、美术&#xff0c;在讨论相关的&#xff0c;人物的曲线路径行走的问题&#xff1b; 一些数学比较牛X的&#xff0c;说了用2阶&#xff0c;或…