Splay 是一种二叉查找树,它通过不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,并且保持平衡而不至于退化为链。它可以在 的时间内完成基于 Splay 操作的修改与查询。
本文提供使用原生指针和数组模拟指针两种方法实现的代码,可以点击代码块上方的切换按钮查看两种不同版本的代码。
#辅助函数 / 类
#node
结构体
以下为每个 Splay 节点的定义。
1 |
|
节点中的 root
表示指向根节点的指针的指针。这样做可以方便从任意一个节点找到整棵 Splay 的根节点,并修改它。
size
表示以当前节点为根的 Splay 共有多少个节点(包括自身),有了 size
,就可以轻松地实现选择和排名操作。
此处在 node
的析构函数中递归释放所有内存以避免内存泄漏。
1 |
|
s
表示以当前节点为根的 Splay 共有多少个节点(包括自身),有了它就可以轻松地实现选择和排名操作。
#关系(relation) / 儿子(child)
为了旋转操作的方便,我们给每个节点设置一个「关系」属性,表示该节点与其父节点的关系,若该节点为左孩子,则「关系」为 0
,反之则为 1
。relation()
方法用来计算这个「关系」,而 child()
方法返回与该节点「关系」为 x
的子节点的引用。
1 |
|
1 |
|
#上传信息(pushup)
易错:不要忘记加上 count
。
1 |
|
1 |
|
#主要操作
#旋转(rotate)
为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。
旋转需要保证:
- 整棵 Splay 的中序遍历不变(不能破坏二叉查找树的性质)。
- 受影响的节点维护的信息依然正确有效。
root
必须指向旋转后的根节点。
在 Splay 中旋转分为两种:左旋和右旋。
以左旋(当前节点为父节点的左儿子为例),旋转分为三个步骤:
- 将祖父节点与自身连接;
- 将自己的右孩子接到自己的父节点的左孩子的位置(替代自己);
- 将父节点接到自己的右孩子的位置。
1 |
|
1 |
|
#Splay
Splay 规定:每访问一个节点后都要强制将其旋转到根节点。此时旋转操作具体分为 6 种情况讨论(其中 为需要旋转到根的节点)
-
如果 的父亲是根节点,直接将 左旋或右旋(图 1, 2)。
-
如果 的父亲不是根节点,且 和父亲节点的「关系」和父亲和父亲的父亲节点的「关系」相同,首先将其父亲左旋或右旋,然后将 右旋或左旋(图 3, 4)。
-
如果 的父亲不是根节点,且 和父亲节点的「关系」和父亲和父亲的父亲节点的「关系」不同,将 左旋再右旋、或者右旋再左旋(图 5, 6)。
如果 Splay 操作的目标为 nullptr
则更新根节点。
1 |
|
1 |
|
#插入(insert)
根据 BST 的性质二分查找插入即可。
1 |
|
1 |
|
为了下文操作中的方便,在建树时可以预先插入两个哨兵节点以防越界。
#查找(find)
同样地,根据 BST 的性质二分查找即可。
1 |
|
1 |
|
#排名(rank)
排名函数返回树中比给定值小的数的个数。
1 |
|
1 |
|
#选择(select)
选择函数返回树中第 大的元素。传入的 应保证不大于树中元素个数。
1 |
|
1 |
|
#节点的前驱(predecessor)和后继(successor)
由 BST 的性质,前驱为左子树中最靠右的节点,后继为右子树中最靠左的节点。
1 |
|
1 |
|
#值的前驱(predecessor)和后继(successor)
使用 find()
函数找到对应的节点,再查询节点的前驱与后继即可。如果不存在则新建一个节点来辅助查询。
1 |
|
1 |
|
#删除(erase)
如果该节点上有多个相同的值,删除其中一个即可。否则删除节点。
1 |
|
1 |
|
#代码
1 |
|
1 |
|
#参考资料
- Splay 学习笔记(一),黄浩睿,2015 年 12 月 20 日。
- Splay,OI Wiki,2022 年 3 月 20 日。