上周五夜里三点多更了一篇博客 初识splay tree (一) ,却只来得及写了一道简单模板题。今天补充一些关于splay tree 的实质性的内容。
splay 的产生:
splay 诞生的出发点是基于节点 [访问局部性] 的启发式优化:
Wiki
伸展树(英语:Splay Tree)是一种二叉查找树,它能在O(log n)内完成插入、查找和删除操作。它是由丹尼尔·斯立特(Daniel Sleator)和罗伯特·塔扬在1985年发明的1。在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
splay可以解决的问题
可以看出splay树同样是二叉查找树的一种,核心在于每次访问都把该节点提至根(通过旋转操作:zig、zag),可以证明这种方法使得各种操作均摊复杂度最坏为log(N),证明过程详细见杨思雨的论文。
假如对于给定的一个序列S,除了现有如下一些操作需要满足:
单点查询、单点删除、单点修改、单点插入、区间删除、区间插入、区间修改、区间查询。
则此时splay就可以派上用途了,以上的每一个操作splay都可以均摊log(N)的时间复杂度内完成(具体如何做后面再说)。这是平衡二叉树(区间操作维护复杂度高)、线段树(不支持插入和删除)所不具备的。
注意:这里操作都是以 位置信息 为偏序关系这一前提,而不是数值信息 。即插入、删除、修改等操作都是以一个位置信息为标志的,比如删除[3,5]指删除序列中第3,4,5三个位置上的数值。
splay stree基本操作
前面提到,每次访问后提至根(伸展操作)是伸展树的关键,这一提至根的操作是通过旋转操作完成的,这里旋转操作和平衡二叉树的旋转本质相同,均是为了保证旋转前后中序遍历一致。
具体来说分为三种情况:
注意: splay 不像平衡二叉树那样严格要求平衡程度,这使得splay操作简单的同时,也必然存在退化成单链表的风险,然而即使如此,其均摊复杂度任然足够优秀,在实际表现中性能不错。
splay关于区间的维护
单点查询、单点删除、单点修改、单点插入、区间删除、区间插入、区间修改、区间查询。
还是刚才谈到的区间维护,这是splay的优势,具体如何使用伸展树实现呢,这里的关键仍然是伸展操作。
试想对于我们要访问的区间[a,b] (a<=b),通过伸展操作,将a-1提至根,将b+1提至根的右孩子,并且在旋转前后中序遍历不变的前提下,根的右孩子的左孩子就是我们想要访问的区间。
~~文中所有截图请允许我偷懒:)
区间修改的Lazy Tag应用
和线段数一样,在进行区间修改时,需要应用LazyTag,同时也为splay引入了pushdown()和update()操作,这本质与线段树无异。可以说如果学习过线段树,学习splay会轻松很多。
值得一提的是应用LazyTag的两个原则:
对于一个节点,访问前要执行pushdown()。
对于一个节点,update前需要对左右孩子进行pushdown()。
第二个其实就是来自第一个。
Splay 典型的练习题目:
具体的代码编写和实现技巧写在第三篇吧~~~
扫码关注作者,定期分享技术、算法类文章