伸展树(Splay Tree)

Freddie

2019-03-05 16:14:15

Personal

### [Splay详解(一)](https://cloud.tencent.com/developer/article/1091979?from=10680) ### [Splay详解(二)](https://cloud.tencent.com/developer/article/1091999) ------------ ## 前言 相对于AVL,Splay的实现更为简捷。伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。 通常在任意数据结构的生命期内,执行不同操作的概率往往极不均衡,而且各操作之间具有极强的相关性,并在整体上多呈现出极强的规律性。其中最为典型的,就是所谓的“数据局部性”(data locality),这包括两个方面的含义: - 刚刚被访问过的元素,极有可能在不久之后再次被访问到 - 将被访问的下一元素,极有可能就处于不久之前被访问过的某个元素的附近 如果将该策略应用于二叉搜索树。只需将刚被访问的节点,及时地“转移”至树根(附近),即可加速后续的操作。当然, 转移前后的搜索树必须相互等价,故为此使用前文介绍的“旋转“等价变换的技巧 ## 逐层伸展 每访问过一个节点之后,随即反复地以它的父节点为轴,经适当的旋转将其提升一层,直至最终成为树根。以下图为例,若深度为3的节点E刚被访问–无论查找或插入,甚至“删除”都可通过3次旋转,将该树等价变换为以E为根的另一棵二叉搜索树 ![](https://cdn.luogu.com.cn/upload/pic/62755.png) 随着节点E的逐层上升,两侧子树的结构也不断地调整,故这一过程也形象地称作伸展 (splaying),而采用这一调整策略的二叉搜索树也因此得名。不过,为实现真正意义上的伸 展树,还须对以上策略做点微妙而本质的改进。之所以必须改进,是因为目前的策略仍存在致命 的缺陷—对于很多访问序列,单次访问的分摊时间复杂度在极端情况下可能高达n。 不难验证,若从空树开始依次插入关键码{ 1, 2, 3, 4, 5 },且其间采用如上调整策略, 则可得到如下图所示的二叉搜索树。 ![](https://cdn.luogu.com.cn/upload/pic/62756.png) 在各次访问之后,为将对应节点伸展调整至树根,分别需做4、4、3、2和1次旋转。 一般地,若节点总数为n,则旋转操作的总次数应为: (n - 1) + { (n - 1) + (n - 2) + … + 1 }= (n2 +n-2)/2 = n的平方。 如此分摊下来,每次访问平均需要n时间。很遗憾,这一效率不仅远远低于AVL树,而且甚至与原始的二叉搜索树的最坏情况相当。 而事实上,问题还远不止于此。稍做比对即不难发现,上图a与f中二叉搜索树的结构完全相同。也就是说,经过以上连续的5次访问之后,全树的结构将会复原! 这就意味着,以上情况可以持续地再现。 当然,这一实例,完全可以推广至规模任意的二叉搜索树。于是对于规模为任意n的伸展树, 只要按关键码单调的次序,周期性地反复进行查找,则无论总的访问次数m >> n有多大,就分摊意义而言,每次访问都将需要n时间! ## 双层伸展 为克服上述伸展调整策略的缺陷,一种简便且有效的方法就是:将逐层伸展改为双层伸展。 具体地,每次都从当前节点v向上追溯两层(而不是仅一层),并根据其父亲p以及祖父g的相对位置,进行相应的旋转。主要以下分三类情况: ### zig-zig/zag-zag 如下图所示, 设v是p的左孩子,且p也是g的左孩子; 设W和X分别是v的左、右子树,Y和Z分别是p和g的右子树。 ![](https://cdn.luogu.com.cn/upload/pic/62757.png) 针对这种情况,首先以节点g为轴做顺时针旋转zig(g),其效果如图(b)所示。然后,再以p 为轴做顺时针旋转zig(p),其效果如图(c)所示。如此连续的两次zig旋转,合称zig-zig调整。 自然地,另一完全对称的情形,v是p的右孩子,且p也是g的右孩子,则可通过连续的 两次逆时针旋转实现调整,合称zag-zag操作。 ### zig-zag/zag-zig 如下图所示,设v是p的左孩子,而p是g的右孩子; 设W是g的左子树,X和Y分别是v的左右子树,Z是p的右子树。 ![](https://cdn.luogu.com.cn/upload/pic/62759.png) 针对这种情况,首先以节点p为轴做顺时针旋转zig(p),其效果如(b)所示。然后,再以g 为轴做逆时针旋转zag(g),其效果如图(c)所示。如此zig旋转再加zag旋转,合称zig-zag调整。 同样地,另一完全对称的情形–v是p的右孩子,而p是g的左孩子—则可通过zag旋转再加zig旋转实现调整,合称zag-zig操作。 zig/zag 若v最初的深度为奇数,则经过若干次双层调整至最后一次调整时, v的父亲p即是树根r。 将v的左、右子树记作X和Y,节点p = r的另一子树记作Z。 此时,只需围绕p = r做顺时针旋转zig(p),即 可如图(b)所示,使v最终攀升至树根,从而结束整个伸展调整的过程。 综合以上各种情况,每经过一次双层调整操作,节点v都会上升两层。若v的初始深度depth(v) 为偶数,则最终v将上升至树根。若depth(v)为奇数,则当v上升至深度为1时,不妨最后再相应 地做一次zig或zag单旋操作。无论如何,经过depth(v)次旋转后,v最终总能成为树根。 回顾最开始的单层伸展的例子中:在可持续重复的过程中,二叉搜索树的高度始终不小于n/2; 而且,至少有一半的节点在接受访问时,不仅没有如最初设想的那样靠近树根,而且反过来恰恰处于最底层。 从树高的角度看,问题根源也可再进一步地解释为:在持续访问的过程中,树高依算术级数逐步从n - 1递减至n/2,然后再逐步递增回到n - 1。 以如下图所示的二叉搜索树为例,在find(1)操作之后,采用逐层调整策略与双层调 整策略的效果,分别如图(a)和图(c)所示。 ![213](https://cdn.luogu.com.cn/upload/pic/62760.png) 可见,最深节点(1)被访问之后再经过双层调整,不仅同样可将该节点伸展至树根,而且同时可使树的高度接近于减半。就树的形态而言,双层伸展策略可“智能”地“折叠”被访问的 子树分支,从而有效地避免对长分支的连续访问。这就意味着,即便节点v的深度为n,双层 伸展策略既可将v推至树根,亦可令对应分支的长度以几何级数(大致折半)的速度收缩。 ---------------------