伸展树(Splay)学习笔记
二叉排序树能够支持多种动态集合操作,它可以被用来表示有序集合,建立索引或优先队列等。因此,在信息学竞赛中,二叉排序树应用非常广泛。
作用于二叉排序树上的基本操作,其时间复杂度均与树的高度成正比,对于一棵有 \(n\) 个节点的二叉树,这些操作在最有情况下运行时间为 \(O( \log_2 n)\)。
但是,如果二叉树退化成了一条 \(n\) 个节点组成的线性链表,则这些操作在最坏情况下的运行时间为 \(O(n)\)。
有些二叉排序树的变形,其基本操作的性能在最坏情况下依然很好,如平衡树(AVL)等。但是,它们需要额外的空间来存储平衡信息,且实现起来比较复杂。同时,如果访问模式不均匀,平衡树的效率就会受到影响,而伸展树却可以克服这些问题。
伸展树(Splay Tree),是对二叉排序树的一种改进。虽然它并不能保证树一直是“平衡”的,但对于它的一系列操作,可以证明其每一步操作的“平摊时间”复杂度都是 \(O(\log_2 n)\) 。平摊时间是指在一系列最坏情况的操作序列中单次操作的平均时间。所以,从某种意义上来说,伸展树也是一种平衡的二叉排序树。而在各种树形数据结构中,伸展树的空间复杂度(不需要记录用于平衡的冗余信息)和编程复杂度也都是很优秀的。
获得较好平摊效率的一种方法就是使用“自调整”的数据结构,与平衡结构或有明确限制的数据结构相比,自调整的数据结构有一下几个优点:
- 从平摊角度上,它们忽略常数因子,因此绝对不会差于有明确限制的数据结构,而且它们可以根据具体使用情况进行调整,所以在使用模式不均匀的情况下更加有效;
- 由于无需存储平衡信息或者其他限制信息,所以所需的存储空间更小;
- 它们的查找和更新的算法与操作都很简单,易于实现。
当然,自调整的数据结构也有其潜在的缺点:
- 它们需要更多的局部调整,尤其在查找期间,而那些有明确限制的数据结构仅需要在更新期间进行调整,查找期间则不需要;
- 一系列查找操作中的某一个可能会耗时较长,这在实时处理的应用程序中可能是一个不足之处。
1. 伸展树的主要操作
伸展树是对二叉排序树的一种改进。与二叉排序树一样,伸展树也具有有序性,即伸展树中的每一个节点 \(x\) 都满足:该节点左子树中的每一个元素都小于 \(x\),而其右子树中的每一个元素都大于 \(x\)。
但是,与普通二叉排序树不同的是,伸展树可以“自我调整”,这就要依靠伸展树的核心操作 —— \(\text{Splay(x, S)}\)。
1.1 伸展操作
伸展操作 \(\text{Splay(x, S)}\) 是在保持伸展树有序的前提下,通过一系列旋转,将伸展树 \(\text{S}\) 中的元素 \(\text{x}\) 调整至数的根部。在调整的过程中,要分以下三种情况分别处理。
情况一:节点 \(\text{x}\) 的父节点 \(\text{y}\) 是根节点。
此时,
- 若 \(\text{x}\) 是 \(\text{y}\) 的左儿子,则我们进行一次右旋操作 \(\text{Zig(x)}\);
- 若 \(\text{x}\) 是 \(\text{y}\) 的右儿子,则我们进行一次左旋操作 \(\text{Zag(x)}\)。
经过旋转,使 \(\text{x}\) 成为二叉排序树 \(S\) 的根节点,且依然满足二叉排序树的性质。
\(\text{Zig}\) 操作和 \(\text{Zag}\) 操作如图所示:
情况二:节点 \(\text{x}\) 的父节点 \(\text{y}\) 不是根节点,且 \(\text{x}\) 和 \(\text{y}\) 同为各自父节点的左儿子,或同为各自父节点的右儿子。
此时,我们设 \(\text{z}\) 为 \(\text{y}\) 的父节点,
- 若 \(\text{x}\) 和 \(\text{y}\) 同时是各自父节点的左儿子,则进行一次 \(\text{Zig-Zig}\) 操作;
- 若 \(\text{x}\) 和 \(\text{y}\) 同时是各自父节点的右儿子,则进行一次 \(\text{Zag-Zag}\) 操作。
如图所示:
情况三:节点 \(\text{x}\) 的父节点 \(\text{y}\) 不是根节点,且 \(\text{x}\) 和 \(\text{y}\) 中的一个是其父节点的左儿子,另一个是其父节点的右儿子。
此时,我们设 \(\text{z}\) 为 \(\text{y}\) 的父节点,
- 若 \(\text{x}\) 是 \(\text{y}\) 的左儿子,\(\text{y}\) 是 \(\text{z}\) 的右儿子,则进行一次 \(\text{Zig-Zag}\) 操作;
- 若 \(\text{x}\) 是 \(\text{y}\) 的右儿子,\(\text{y}\) 是 \(\text{z}\) 的左二子,则进行一次 \(\text{Zag-Zig}\) 操作。
下面举一个例子来体会上面的伸展操作。
如下图所示,最左边的一个单链先执行 \(\text{Splay(1, S)}\),我们将元素 \(1\) 调整到了伸展树的根部。
执行几次 \(\text{Splay(1, S)}\) 的效果
然后再执行 \(\text{Splay(2, S)}\),将元素 \(2\) 调整到伸展树 \(\text{S}\) 的根部。如下图所示:
执行几次 \(\text{Splay(2, S)}\) 的效果
1.2 伸展树的基本操作
利用伸展树 Splay ,我们可以在伸展树 \(S\) 上进行如下几种基本操作。
(1) \(\text{Find(x, S)}\):判断元素 \(\text{x}\) 是否在伸展树 \(\text{S}\) 表示的有序集中。
首先,与在二叉排序树中进行查找操作操作一样,在伸展树中查找元素 \(\text{x}\)。如果 \(\text{x}\) 在树中,则再执行 \(\text{Splay(x, S)}\) 调整伸展树。
(2) \(\text{Insert(x, S)}\):将元素 \(\text{x}\) 插入到伸展树 \(S\) 表示的有序集中。
首先,与在二叉排序树中进行插入操作一样,将 \(\text{x}\) 插入到伸展树 \(\text{S}\) 中的相应位置,再执行 \(\text{Splay(x, S)}\) 调整伸展树。
(3) \(\text{Join(S1, S2)}\):将两棵伸展树 \(\text{S1}\) 与 \(\text{S2}\) 合并成为一棵伸展树。其中,\(S1\) 的所有元素都小于 \(S2\) 的所有元素。
首先,找到伸展树 \(S1\) 中最大的一个元素 \(\text{x}\),再通过 \(\text{Splay(x, S1)}\) 将 \(\text{x}\) 调整到伸展树 \(S1\) 的根部。然后将 \(S2\) 作为 \(\text{x}\) 节点的右子树插入,这样就得到了新的伸展树 \(S\),如图所示:
\(\text{Join(S1, S2)}\) 的两个步骤
(4)\(\text{Delete(x, S)}\):将元素 \(x\) 从伸展树 \(S\) 所表示的有序集中删除。
首先,执行 \(\text{Find(x, S)}\) 将 \(\text{x}\) 调整为根节点,然后再对左右子树执行 \(\text{Join(S1, S2)}\) 操作即可。
(5)\(\text{Split(x, S)}\):以 \(x\) 为界,将伸展树 \(S\) 分离为两棵伸展树 \(S1\) 和 \(S2\),其中,\(S1\) 的所有元素都小于 \(x\),\(S2\) 的所有元素都大于 \(x\)。
首先,执行 \(\text{Find(x, S)}\) 将 \(\text{x}\) 调整为根节点,则 \(\text{x}\) 的左子树就是 \(\text{S1}\),右子树就是 \(\text{S2}\)。如图所示:
除了上述介绍的 \(5\) 种基本操作外,伸展树还支持求最大值、最小值、求前趋、求后继等多种操作,这些操作也都是建立在伸展树操作 \(\text{Splay}\) 的基础之上的。
2. 伸展树的算法实现
注:这里的代码并不是最简单的代码,而是基于上述思想实现的代码,更方便我们结合之前分析的内容来理解。
下面给出伸展树的各种操作的算法实现,它们都是基于如下伸展树的类型定义:
int lson[maxn], // 左儿子编号
rson[maxn], // 右儿子编号
p[maxn], // 父节点编号
val[maxn], // 节点权值
sz; // 编号范围 [1, sz]
struct Splay {
int rt; // 根节点编号
void zag(int x); // 左旋
void zig(int x); // 右旋
void splay(int x); // 伸展操作:将x移到根节点
int func_find(int v); // 查找是否存在值为v的节点
void func_insert(int v); // 插入
void func_delete(int v); // 删除
int get_max(); // 求最大值
int get_min(); // 求最小值
int get_pre(int v); // 求前趋
int get_suc(int v); // 求后继
int join(int rt1, int rt2); // 合并
} tree;
1. 左旋操作
void Splay::zag(int x) {
int y = p[x], z = p[y], a = lson[x];
lson[x] = y; p[y] = x;
rson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
Zag(x)操作
2. 右旋操作
void Splay::zig(int x) {
int y = p[x], z = p[y], a = rson[x];
rson[x] = y; p[y] = x;
lson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
Zig(x)操作
3. 伸展操作
void Splay::splay(int x) {
while (p[x]) {
int y = p[x], z = p[y];
if (!z) {
if (x == lson[y]) zig(x);
else zag(x);
}
else if (lson[y] == x) {
if (lson[z] == y) { // zig-zig
zig(y);
zig(x);
}
else { // zig-zag
zig(x);
zag(x);
}
}
else { // rson[y] == x
if (lson[z] == y) { // zag-zig
zag(x);
zig(x);
}
else { // zag-zag
zag(y);
zag(x);
}
}
}
rt = x;
}
4. 查找
int Splay::func_find(int v) {
int x = rt;
while (x) {
if (val[x] == v) {
rt = x;
splay(x);
return x;
}
else if (v < val[x]) x = lson[x];
else x = rson[x];
}
return 0; // 返回0说明没找到
}
5. 插入
void Splay::func_insert(int v) {
val[++sz] = v;
if (rt == 0) {
rt = sz;
return;
}
int x = rt;
while (true) {
if (v < val[x]) {
if (lson[x]) x = lson[x];
else {
lson[x] = sz;
p[sz] = x;
break;
}
}
else {
if (rson[x]) x = rson[x];
else {
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(rt = sz);
}
6. 删除(会用到下面定义的join操作)
void Splay::func_delete(int v) {
int x = func_find(v);
if (!x) return;
int ls = lson[x], rs = rson[x];
lson[x] = rson[x] = 0;
p[ls] = p[rs] = 0;
rt = join(ls, rs);
}
7. 求最大值
int Splay::get_max() {
if (!rt) return 0;
int x = rt;
while (rson[x]) x = rson[x];
splay(rt = x);
return x;
}
8. 求最小值
int Splay::get_min() {
if (!rt) return 0;
int x = rt;
while (lson[x]) x = lson[x];
splay(rt = x);
return x;
}
9. 求前趋
int Splay::get_pre(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] <= v) {
if (!ans || val[ans] < val[x]) ans = x;
if (rson[x]) x = rson[x];
else break;
}
else {
if (lson[x]) x = lson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
10. 求后继
int Splay::get_suc(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] >= v) {
if (!ans || val[ans] > val[x]) ans = x;
if (lson[x]) x = lson[x];
else break;
}
else {
if (rson[x]) x = rson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
11. 合并
int Splay::join(int rt1, int rt2) {
if (!rt1) return rt2;
if (!rt2) return rt1;
Splay tree1;
tree1.rt = rt1;
rt1 = tree1.get_max();
assert(rson[rt1] == 0);
rson[rt1] = rt2;
p[rt2] = rt1;
return rt1;
}
示例代码(对应题目:《怪物仓库管理员(二)》):
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500050;
int lson[maxn], // 左儿子编号
rson[maxn], // 右儿子编号
p[maxn], // 父节点编号
val[maxn], // 节点权值
sz; // 编号范围 [1, sz]
struct Splay {
int rt; // 根节点编号
void zag(int x); // 左旋
void zig(int x); // 右旋
void splay(int x); // 伸展操作:将x移到根节点
int func_find(int v); // 查找是否存在值为v的节点
void func_insert(int v); // 插入
void func_delete(int v); // 删除
int get_max(); // 求最大值
int get_min(); // 求最小值
int get_pre(int v); // 求前趋
int get_suc(int v); // 求后继
int join(int rt1, int rt2); // 合并
} tree;
/**
zag(int x) 左旋
*/
void Splay::zag(int x) {
int y = p[x], z = p[y], a = lson[x];
lson[x] = y; p[y] = x;
rson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
/**
zig(int x) 右旋
*/
void Splay::zig(int x) {
int y = p[x], z = p[y], a = rson[x];
rson[x] = y; p[y] = x;
lson[y] = a; p[a] = y;
p[x] = z;
if (z) {
if (lson[z] == y) lson[z] = x;
else rson[z] = x;
}
}
/**
splay(int x) 伸展操作
*/
void Splay::splay(int x) {
while (p[x]) {
int y = p[x], z = p[y];
if (!z) {
if (x == lson[y]) zig(x);
else zag(x);
}
else if (lson[y] == x) {
if (lson[z] == y) { // zig-zig
zig(y);
zig(x);
}
else { // zig-zag
zig(x);
zag(x);
}
}
else { // rson[y] == x
if (lson[z] == y) { // zag-zig
zag(x);
zig(x);
}
else { // zag-zag
zag(y);
zag(x);
}
}
}
rt = x;
}
int Splay::func_find(int v) {
int x = rt;
while (x) {
if (val[x] == v) {
rt = x;
splay(x);
return x;
}
else if (v < val[x]) x = lson[x];
else x = rson[x];
}
return 0; // 返回0说明没找到
}
void Splay::func_insert(int v) {
val[++sz] = v;
if (rt == 0) {
rt = sz;
return;
}
int x = rt;
while (true) {
if (v < val[x]) {
if (lson[x]) x = lson[x];
else {
lson[x] = sz;
p[sz] = x;
break;
}
}
else {
if (rson[x]) x = rson[x];
else {
rson[x] = sz;
p[sz] = x;
break;
}
}
}
splay(rt = sz);
}
void Splay::func_delete(int v) {
int x = func_find(v);
if (!x) return;
int ls = lson[x], rs = rson[x];
lson[x] = rson[x] = 0;
p[ls] = p[rs] = 0;
rt = join(ls, rs);
}
int Splay::get_max() {
if (!rt) return 0;
int x = rt;
while (rson[x]) x = rson[x];
splay(rt = x);
return x;
}
int Splay::get_min() {
if (!rt) return 0;
int x = rt;
while (lson[x]) x = lson[x];
splay(rt = x);
return x;
}
int Splay::get_pre(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] <= v) {
if (!ans || val[ans] < val[x]) ans = x;
if (rson[x]) x = rson[x];
else break;
}
else {
if (lson[x]) x = lson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
int Splay::get_suc(int v) {
if (!rt) return 0;
int x = rt, ans = 0;
while (true) {
if (val[x] >= v) {
if (!ans || val[ans] > val[x]) ans = x;
if (lson[x]) x = lson[x];
else break;
}
else {
if (rson[x]) x = rson[x];
else break;
}
}
if (ans) splay(rt = ans);
return ans;
}
int Splay::join(int rt1, int rt2) {
if (!rt1) return rt2;
if (!rt2) return rt1;
Splay tree1;
tree1.rt = rt1;
rt1 = tree1.get_max();
assert(rson[rt1] == 0);
rson[rt1] = rt2;
p[rt2] = rt1;
return rt1;
}
int n, op, x;
int main() {
cin >> n;
while (n --) {
cin >> op;
if (op != 3 && op != 4) cin >> x;
if (op == 1) tree.func_insert(x);
else if (op == 2) tree.func_delete(x);
else if (op == 3) cout << val[tree.get_min()] << endl;
else if (op == 4) cout << val[tree.get_max()] << endl;
else if (op == 5) cout << val[tree.get_pre(x)] << endl;
else cout << val[tree.get_suc(x)] << endl;
}
return 0;
}
伸展树(Splay)学习笔记的更多相关文章
- 树-伸展树(Splay Tree)
伸展树概念 伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由Daniel Sleator和Robert Tarjan创造. (01) 伸展树属于二 ...
- splay学习笔记
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.(来自百科) 伸展树的操作主要是 –rotate(x) 将x旋转到x的父亲的位置 voi ...
- 纸上谈兵: 伸展树 (splay tree)[转]
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 我们讨论过,树的搜索效率与树的深度有关.二叉搜索树的深度可能为n,这种情况下,每 ...
- K:伸展树(splay tree)
伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(lgN)内完成插入.查找和删除操作.在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使 ...
- 高级搜索树-伸展树(Splay Tree)
目录 局部性 双层伸展 查找操作 插入操作 删除操作 性能分析 完整源码 与AVL树一样,伸展树(Splay Tree)也是平衡二叉搜索树的一致,伸展树无需时刻都严格保持整棵树的平衡,也不需要对基本的 ...
- 平衡树splay学习笔记#1
这一篇博客只讲splay的前一部分的操作(rotate和splay),后面的一段博客咕咕一段时间 后一半的博客地址:[传送门] 前言骚话 为了学lct我也是拼了,看了十几篇博客,学了将近有一周,才A掉 ...
- 平衡树splay学习笔记#2
讲一下另外的所有操作(指的是普通平衡树中的其他操作) 前一篇的学习笔记连接:[传送门],结尾会带上完整的代码. 操作1,pushup操作 之前学习过线段树,都知道子节点的信息需要更新到父亲节点上. 因 ...
- [Splay][学习笔记]
胡扯 因为先学习的treap,而splay与treap中有许多共性,所以会有很多地方不会讲的很细致.关于treap和平衡树可以参考这篇博客 关于splay splay,又叫伸展树,是一种二叉排序树,它 ...
- 【BBST 之伸展树 (Splay Tree)】
最近“hiho一下”出了平衡树专题,这周的Splay一直出现RE,应该删除操作指针没处理好,还没找出原因. 不过其他操作运行正常,尝试用它写了一道之前用set做的平衡树的题http://codefor ...
- [Splay伸展树]splay树入门级教程
首先声明,本教程的对象是完全没有接触过splay的OIer,大牛请右上角.. 首先引入一下splay的概念,他的中文名是伸展树,意思差不多就是可以随意翻转的二叉树 PS:百度百科中伸展树读作:BoGa ...
随机推荐
- python技巧 namedtuple
python的namedtuple可以创建一个带字段名的元祖和一个带名字的类 In [1]: from collections import namedtuple ...: ...: nginx=na ...
- A Mountaineer 最详细的解题报告
题目来源:A Mountaineer (不知道该链接是否可以直接访问,所以将题目复制下来了) 题目如下: D - A Mountaineer Time limit : 2sec / Stack lim ...
- 神经网络结构:DenseNet
论文地址:密集连接的卷积神经网络 博客地址(转载请引用):https://www.cnblogs.com/LXP-Never/p/13289045.html 前言 在计算机视觉还是音频领域,卷积神经网 ...
- 数据结构C语言实现----出栈操作
代码如下: #include<stdio.h> #include<stdlib.h> typedef struct { char *base; char *top; int s ...
- k8s极简史:K8s多集群技术发展的历史、现状与未来
引子 随着云原生技术的普及,越来越多的企业使用Kubernetes来管理应用,并且集群规模也呈爆发式增长,企业也亟需应对随集群规模增长而带来的各种挑战.同时,为了更好地提供高可用.弹性伸缩的应用,企业 ...
- CppUnit使用和源码解析
前言 CppUnit是一个开源的单元测试框架,支持Linux和Windows操作系统,在linux上可以直接进行源码编译,得到动态库和静态库,直接链接就可以正常使用,在Windows上可以使用VC直接 ...
- python读取hdfs并返回dataframe教程
不多说,直接上代码 from hdfs import Client import pandas as pd HDFSHOST = "http://xxx:50070" FILENA ...
- 宽度优先搜索--------迷宫的最短路径问题(dfs)
宽度优先搜索运用了队列(queue)在unility头文件中 源代码 #include<iostream>#include<cstdio>#include<queue&g ...
- 定长比较环状字符串-------strcat与strncpy的妙用
题目链接:https://vjudge.net/problem/UVA-1584 题解:用strcpy与strcmp定长比较字符串即可,strcat与strcmp对string不适用,所以只能改写为c ...
- 面试题六十:n个骰子的点数
把n个骰子扔在地上,求出现和为s的概率 可得n<=s<=6n 方法:定义6n-n+1长度的数组,然后对所有可能出现的组合进行计算,把结果进行计数存进数组:递归 方法二:动态规划,大问题小化 ...