本文参考资料:
From yyb:Link
正文:关于 SPLAY
其实我更偏向于把 splay 叫做 cosplay
讲平衡树总逃不过 BST(Binary Search Tree),二叉搜索树,以下是 BST 的性质:
一棵合法的 BST 每个节点上都带有一个数值,我们将其称为节点的 “关键码”。那么对于一棵 BST 上的任意节点,满足:
- 该节点的关键码不小于它左子树的任意结点的关键码
-
该结点的关键码不大于它右子树的任意结点的关键码
显然
,BST 的中序遍历是一个递增的序列
建立一棵 BST
因为笔者很懒,不想到处判边界,所以我们一般可以在一棵空的 BST 中预先插入两个结点,一个正无穷,一个负无穷,如图:
以上是建树的代码
那么,BST 就讲到这里
平衡树的诞生
当 BST 形成一条链的时候,每次查询会变成 O(n2)
这种深度过深的 BST 是不平衡的。所以我们需要一种能保持树的深度在 log(n)的数据结构,于是便诞生了平衡树
SPLAY
splay,又称 cosplay 伸展树,有 “序列之王” 的美称,常数巨大,跑的没有 fhq−treap快,但这不在我们的讨论范围以内
想象一下这样一颗 BST,我们先把它们的大小关系列出来。
Y<Z,C>Y,X<Y,A<X,B>X
对于这样一颗 BST,我们可以通过一些特殊的方式来改变它的形态保持中序序列不变,这也是平衡树的精髓。
怎么改变呢?
旋转
旋转不转不是中国人,这是 splay 的精髓所在。
现在我们的目标是让 X 节点往上爬到它父亲节点 Y 处,让 Y 变成 X 的幺儿,也就是让 Y 节点下降。
这个过程首先要满足 BST 性质
通过例图来思考,此时的 X 节点是 Y 节点的左儿子,小于 Y 节点,为了不改变中序序列,我们可以让 Y 节点成为 X 的右儿子。那么问题来了:变换后的 Y 的确大于 X,但 X 还有一颗右子树呢!
别急,再回想一下 BST 的性质,任意节点大于其左子树中的任意节点,也就是说我们可以把 X 的左子树 B 拿给 Y 当左子树。
好了!世界核平了!Tarjan 放心了
展示一下旋转的成果吧!
旋转前:
旋转后
别高兴得太早!
这只是一种情况,我们需要的是通用
这里有一个小技巧,即:
odd⨁1=odd−1
even⨁1=even+1
这个性质的证明很简单:
即得易见平凡,仿照上例显然。
留作习题答案略,读者自证不难。
反之亦然同理,推论自然成立,略去过程 QED,由上可知证毕。
Just a joke
设节点 Y 为 X 的父亲,Y 的 w(0 代表左儿子,1 代表右儿子) 儿子
- step1: 将 Y 节点放到 X 节点的 w⨁1 的位置
- step2: 如果 X 的 w⨁1 位置上有一颗子树,放在 Y 的 w 位置上
仅仅有 rotate 操作还不够,splay 到目前为止依然很容易被卡。
想象这样一棵树:
发现无论怎么旋转 X 都不能使得这棵树最长的一条链变短。我们称这种 X,X 它爹,X 它爹它爹在一条线上的情况称为三点共线。
怎么办呢?可怜的 splay 被人溜了
办法还是有滴
- step1: 如果三点共线,我们可以先旋转 X 它爹,这样便可以使其更加 “平衡”
- step2: 如果不共线……不共线……那就旋转 X 就好了
这便是 splay 操作
至此,splay 就差不多讲完了,那么再来一道例题吧
- 题面:
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
1. 插入 x 数
2. 删除 x 数 (若有多个相同的数,因只删除一个)
3. 查询 x 数的排名 (排名定义为比当前数小的数的个数 +1 )
4. 查询排名为 x 的数
5. 求 x 的前驱 (前驱定义为小于 x,且最大的数)
6. 求 x 的后继 (后继定义为大于 x,且最小的数)
插入操作
首先我们先查找 BST 当中有没有和需要插入的节点关键码相同的节点,如果有,就把当前节点的 “副本” 数+1
如果没有,就遍历到叶子节点,再新增一个节点就好了
查找操作
设查找节点的关键码为 x,如果 x 大于当前节点的关键码,就往右子树跑,否则往左子树找。找到后把当前节点 splay 到根,保证 BST 的平衡
前驱/后继操作
首先执行 find 操作。
以前驱为例,当前的根节点就是 x 的父节点,所以如果 root 的关键码大于 x,那么 root 就是 x 的前驱。否则就跳到左儿子找,再反着跳就好了
删除操作
找到这个数的 last,把他 splay 到根节点
然后找到这个数 next,把他 splay 到 last 的底下
然后……然后就没有了呀……
比 last 大是 next
比 next 小的且比 last 大的只有当前的节点
在 next 的左幺儿上面,
だから直接把 root 右幺儿的左幺儿删掉就可以了
第 K 大
现在再来看已经十分简单了
首先如果左子树的大小加上本身的个数大于 k,直接在左子树里找就行了
否则就把 k 减去左子树大小再减去本身的个数,再在右子树里找就行了
完整代码:
3 条评论
ljx · 2020年1月20日 10:14 下午
与前驱后继时特判同理
ljx · 2020年1月20日 10:13 下午
opt=3 时,应该要特判根节点的值是否小于 x
Remmina · 2019年12月19日 11:14 下午
很抱歉咕了您这么久,主要是最近马上期末考试了站长同学真没啥时间去审核,而 Qiuly 同学可能也有些事吧 QAQ
非常抱歉 Orz
另外感谢您投稿的优质文章!(* ̄︶ ̄)