伸展树Splay

平衡树的旋转

一般的平衡树通过旋转来维持树的动态平衡。

回顾二叉搜索树的性质,无论什么时候都需要保证左子节点小于根节点,右子节点大于根节点。我们需要在维护平衡的过程中保持该性质不变。

旋转分为左旋与右旋。

总结起来,树的旋转需要以下几步:

1.(以如图右旋为例)将Q的左儿子设定为B。

2.将P的右儿子设定为Q

3.如果旋转以后P称为了根节点,进行特殊判断。

很明显,旋转以后仍然保持BST的性质。

Splay的伸展

当我们把某一个点x一路旋转至某个位置时,我们把这称为x的一次伸展。Splay通过将查询频率高的节点伸展至根节点,来提高操作效率。可见Splay的平衡具有随机性,是一种动态平衡。

如何伸展?一种很自然的想法:x不停旋转到父亲的位置,直到到达指定节点。但是这样的操作不够优。我们可以考虑一次性进行两次旋转来让伸展之后的树更平衡。

我们一般进行如下的分类讨论:

  1. 若x的父节点即为伸展目标:直接旋转x即可。
  2. 若x的父节点不是伸展目标,且x、x的父亲、x的父亲的父亲三点共线(即x是x的父亲的左儿子,x的父亲的父亲也是x的父亲的左儿子; 或x是x的父亲的右儿子,x的父亲的父亲也是x的父亲的右儿子)。这时先旋转x的父亲,再旋转x。
  3. 若三点不共线,对x进行两次旋转即可。

仔细观察发现,我们仅仅只优化了三点共线的情况。为什么这样做有用?试想一种链状

的情况。若单旋,则最终还是一条链。若双旋,情况就不一样了。

Splay的插入、删除与前驱后继的求解

Splay的插入

Splay的插入几乎与BST相同。但是多了一步,就是插入完成之后将插入的节点伸展至根节点,以加速操作效率。

Splay的删除

相比插入,Splay的删除较为复杂。分一下步骤:

  1. 找到要删除的节点,将其旋转至根。
  2. 若x只有一个儿子,那么直接处理即可。
  3. 若x有两个儿子,我们则需要找一个合适的节点来代替x。我们采用前驱。我们通过与BST一样的方式来查找前驱。但由于x是根节点了,所以就可以不用考虑前驱在上面的情况。直接找到左子树中的最大值即可。找到前驱之后,将前驱伸展至根的左子节点(只有可能在左边)
  4. 注意:由于前驱是左子树中最大的,所以前驱不存在右儿子。此时我们直接将前驱更新为根节点即可。

Splay的前驱与后继

完全可以按照BST的做法去求解。但是如果要查找前驱的值根本不存在于数中呢?例如:假设数中已经有2,4,6,8四个节点,要查找5的前驱。很明显是4,但是怎么找呢?

我们可以将5插入到这棵树中(注意5会被伸展到根),然后查找左子树中的最大值,最后将5删除。通过这样简便的方法求解前驱即可。后继也一样。

用Splay求解排名有关问题

求解x的排名

求解x的排名可以这样做:将x伸展到根,然后求出左子树中的节点个数+1.(左子树中的节点个数即为小于x的数的个数)

问题转化为了如何求解一个节点的儿子总数?

针对这个问题,我们可以在splay的结构体中加入size和cnt两个成员。Size统计包括当前节点的儿子总数,cnt统计值为当前节点的值的数有几个。例如存在五个5,而平衡树绝对不允许存在相同的数,这时候只能利用cnt来统计了。

所以对于一个节点x,x的size值 = 左儿子的size + 右儿子的size + x的cnt。

而问题不会那么简单。在旋转的过程中,size的值会不停改变。

其实手画一棵平衡树模拟一下就会知道:

在一次旋转中,只有被旋转的节点&被旋转的节点的父亲 的size会改变。

所以我们只需要在每一次旋转之后更新一下这两个节点的size值就可以了。

查询排名为k的值

查找排名为k的值听上去很复杂,但是有了size和cnt的帮助,问题也就不那么难了。

从根节点开始向下查询。分三类:

  1. 若当前节点的左儿子的size就已经>=k了,直接往左边查询。
  2. 若左儿子的size < k,但是左儿子的size加上当前节点的cnt大于等于k,说明排名为k的值就是当前节点。
  3. 对于剩下的情况(也就是左儿子的size加上当前节点的cnt的总和还是不足k),那么向右查找。这时候,我们相当于在右子树中有了一个全新的起点。我们只需要在右子树中查找出排名为(k - 左儿子的size – 当前节点的cnt)的值即可。而这只需要依靠前面的方法就可以了。

区间翻转问题

Splay最大的优点就是可以利用它求解区间有关的问题。

给出一个1~n的有序序列,要求进行m次区间翻转(例如翻转1,2,3,4,5的[2,4]区间,就会得到1,4,3,2,5.)。要求输出m次翻转以后的序列。——注意,翻转之后的序列不一定有序。

很显然,既然会存在翻转,我们一定不能再按照点的权值来构造Splay了。换一种思路,利用它的排名来构造Splay。例如1,4,3,2,5,  4的排名是2,所以它在Splay中的值实际上代表2.

继续考虑如何找到一个特定区间。根据Splay的性质,我们容易得到根节点的右儿子的左子树一定大于根节点,却小于根节点的右儿子。根据这一性质,如果我们要查询区间[l, r](注意这里的[l, r]表示排名第l至排名第r的所有点,并非值为l与r之间的所有点。例如在1,4,3,2,5中,[2,3]代表的是4,3两个点),就可以把l-1首先伸展到根,r+1伸展到根的右儿子(r>l),那么r+1的左子树就是l~r的所有了。

但是考虑到如果需要翻转[1, n]怎么办,并不存在排名为0和n+1的节点。这时候的处理方法一般是加入哨兵节点。即插入inf与-inf。这样在做[1,x]的时候先把-inf伸展到根,在伸展x+1即可。

现在具体考虑如何翻转。既然以在序列中值的排名作为标准来建树,那么翻转一个区间相当于交换这个区间的每一个节点的左儿子与右儿子。考虑一下现在的Splay,对于每一个节点u,翻转相当于原来在u之后的点全部都到了u的前面,原来在u之前的点全部到了u的后面。这也可以形象在树上表示出来,也就是对于一个节点u,交换其所有节点的左右儿子。

但是这样很明显会超时的,所以我们借鉴之前线段树的做法:增设一个翻转懒标记lazy tag。当对于一个区间要全部进行翻转的时候,在这个区间的根节点上打上懒标记。在标记下传的时候交换左右子树即可。

标记什么时候下传?当需要查找x的时候,自顶向下查找,若沿途发现了未下传的标记,即下传。保证查找的x的排名正确即可。

最后的答案怎么来找?鉴于有可能标记可能未曾全部下传,我们选择查找排名为2~n+1(哨兵节点的存在)的点,在这个过程中就能保证答案的正确性了。

猜你喜欢

转载自www.cnblogs.com/qixingzhi/p/9194657.html