伸展树(Splay Tree)是AVL树不错的替代,它有以下几个特点:
(1)它是二叉查找树的改进,所以具有二叉查找树的有序性。
(2)对伸展树的操作的平摊复杂度是O(log2n)。
(3)伸展树的空间要求、编程难度非常低。
提到伸展树,就不得不提到AVL树和Read-Black树,虽然这两种树能够保证各种操作在最坏情况下都为logN,但是两都实现都比较复杂。而在实际情况中,90%的访问发生在10%的数据上。因此,我们可以重构树的结构,使得被经常访问的节点朝树根的方向移动。尽管这会引入额外的操作,但是经常被访问的节点被移动到了靠近根的位置,因此,对于这部分节点,我们可以很快的访问。这样,就能使得平摊复杂度为logN。
1、自底向上的伸展树
伸展操作Splay(x,S)是在保持伸展树有序性的前提下,通过一系列旋转操作将伸展树S中的元素x调整至树的根部的操作。
在旋转的过程中,要分三种情况分别处理:
(1)Zig 或 Zag
(2)Zig-Zig 或 Zag-Zag
(3)Zig-Zag 或 Zag-Zig
1.1、Zig或Zag操作
节点x的父节点y是根节点。
1.2、Zig-Zig或Zag-Zag操作
节点x的父节点y不是根节点,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。
1.3、Zig-Zag或Zag-Zig操作
节点x的父节点y不是根节点,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。
2.以上的操作我们就可以将某一个节点进行相应的旋转,接下来叙述一下如何对于区间进行操作。
首先我们要知道,伸展树是具有有序性的,无论进行如何反转操作,有序性都不会被破坏掉。这样子就对我们的区间操作提供了很大的方便,假设要对于区间【L,R】进行操作,我们先把L-1反转到根节点处,再把R+1节点反转到根的右孩子处,这样很显然的目前R节点的左孩子的子树就是我们要处理的这段区间,由于边界问题,我们经常加入两个虚拟节点,分别是数列的头部和尾部,然后就可以方便的进行操作了,把一棵子树剪切,翻转等操作都可以利用这个性质很好的实现。有时候为了边界问题,我们会添加两个虚节点,分别作为序列的开始和结尾。接下来是我的splay的实现代码,仅供参考。
#define N 500000
#define lc (tr[id].c[0])
#define rc (tr[id].c[1])
struct Tr {
int fa, sum, val, c[2], lz;
}tr[N];
int newtr(int k, int f) {//新建立一个节点
tr[tot].sum = 1, tr[tot].val = k;
tr[tot].c[0] = tr[tot].c[1] = -1;
tr[tot].lz = 0;
tr[tot].fa = f;
return tot++;
}
void Push(int id) {
int lsum, rsum;
lsum = (lc == -1)?0:tr[lc].sum;
rsum = (rc == -1)?0:tr[rc].sum;
tr[id].sum = lsum+rsum+1;
}
void lazy(int id) {//懒操作
if (tr[id].lz) {
swap(lc, rc);
tr[lc].lz ^= 1, tr[rc].lz ^= 1;
tr[id].lz = 0;
}
}
int build(int l, int r, int f) {//建树
if (r < l) return-1;
int mid = l+r>>1;
int ro = newtr(data[mid], f);
tr[ro].c[0] = build(l, mid-1, ro);
tr[ro].c[1] = build(mid+1, r, ro);
Push(ro);
return ro;
}
void Rotate(int x, int k) {
if (tr[x].fa == -1) return;
int fa = tr[x].fa, w;
lazy(fa), lazy(x);
tr[fa].c[!k] = tr[x].c[k];
if (tr[x].c[k] != -1) tr[tr[x].c[k]].fa = fa;
tr[x].fa = tr[fa].fa, tr[x].c[k] = fa;
if (tr[fa].fa != -1) {
w = tr[tr[fa].fa].c[1]==fa;
tr[tr[fa].fa].c[w] = x;
}
tr[fa].fa = x;
Push(fa);
Push(x);
}
void Splay(int x, int goal) {//将x节点转到goal的儿子上
if (x == -1) return;
lazy(x);
while (tr[x].fa != goal) {
int y = tr[x].fa;
lazy(tr[y].fa), lazy(y), lazy(x);
bool w = x==tr[y].c[1];
if (tr[y].fa != goal && w == (y==tr[tr[y].fa].c[1]))
Rotate(y, !w);
Rotate(x, !w);
}
if (goal == -1) root = x;
Push(x);
}
int find(int k) {//找到第k哥节点的ID
int id = root;
while (id != -1) {
lazy(id);
int lsum = (lc==-1)?0:tr[lc].sum;
if (lsum >= k) {
id = lc;
}
else if (lsum+1 == k) break;
else {
k = k-lsum-1;
id = rc;
}
}
return id;
}
int Index(int l, int r) {//将区间【l+1, r-1】化成一颗子树
Splay(find(l),-1);
Splay(find(r),root);
}
int Getnext(int id) {//寻找后继节点
lazy(id);
int p = tr[id].c[1];
if (p == -1) return id;
lazy(p);
while (tr[p].c[0] != -1) {
p = tr[p].c[0];
lazy(p);
}
return p;
}
int del(int l, int r) {//将【l,r】切掉,返回切掉子树的根节点
Index(l-1, r+1);
int ro = KEY;
tr[KEY].fa = -1;
KEY = -1;
Push(tr[root].c[1]);
Push(root);
return ro;
}
void cut(int k, int ro) {//将子树ro接到第k个树之后
Index(k, k+1);
KEY = ro;
tr[ro].fa = tr[root].c[1];
Push(tr[root].c[1]);
Push(root);
}
void filp(int l, int r) {//对区间【l,r】反转
Index(l-1, r+1);
lazy(root), lazy(tr[root].c[1]);
tr[KEY].lz ^= 1;
}
void Add(int l, int r, int d) {//区间【l,r】的数加上d
Index(l-1, r+1);
tr[KEY].add += d;
tr[KEY].mi += d;
tr[KEY].val += d;
Push(tr[root].c[1]);
Push(root);
}
void Delete(int x) {//删除第x个数
Index(x-1, x+1);
tr[KEY].fa = -1;
tr[tr[root].c[1]].c[0] = -1;
Push(tr[root].c[1]);
Push(root);
}
void Insert(int l, int x) {//在l之后插入x
Index(l, l+1);
int ro;
ro = newtr(x, tr[root].c[1]);
KEY = ro;
Push(tr[root].c[1]);
Push(root);
}
void Revolve(int l, int r, int d) {//【l, r】整体右移d位
int ro = del(r+1-d, r);
cut(l-1, ro);
}