伸展树(Splay)
Splay 是一种二叉查找树,它通过不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,并且保持平衡而不至于退化为链。它由 Daniel Sleator 和 Robert Tarjan 发明。
旋转
旋转(Rotate)的最终目的是将该节点的深度 − 1 -1 −1并且保证二叉搜索树的结构不改变。
根据节点 x x x是父节点的左子节点或者是右子节点分为右旋和左旋转。
我们以右旋为例,我们发现,如果我们想让节点 X X X的深度 − 1 -1 −1,我们必须让 X X X做 Z Z Z的子节点,至于是左右子节点,取决于 Y Y Y的位置,因为 X X X是代替 Y Y Y的位置。那么 b b b就可以做 Y Y Y的左节点,进而 Y Y Y做 X X X的右节点。
上述过程就是节点右旋的过程,我们将 X X X的深度 − 1 -1 −1而不改变二叉搜索树的性质。
而左旋正好和右旋对称,读者可以自己尝试推导一下。
这两种情况对应的选择操作也叫做Zig。
我们可以将这两个操作结合成一个函数,让这个函数自己判断是左旋还是右旋。
void rotate(int x)
{
// 获取父节点和祖父节点
int y = tree[x].fa;
int z = tree[y].fa;
// 连接祖父节点和X
if (tree[z].l == y)
tree[z].l = x;
else
tree[z].r = x;
tree[x].fa = z;
// 连接父节点和X的子节点,连接父节点和X
if (tree[y].l == x)
{
tree[y].l = tree[x].r;
tree[tree[x].r].fa = y;
tree[x].r = y;
tree[y].fa = x;
}
else
{
tree[y].r = tree[x].l;
tree[tree[x].l].fa = y;
tree[x].l = y;
tree[y].fa = x;
}
}
伸展
伸展(Splay)是伸展树的核心操作,其最终目的是将节点 X X X一直向上选择直到作为节点 R R R的子节点为止或者将 X X X一直旋转到树的根节点为止。
除了完成最终目的,我们还要尽可能的降低BST的深度。
伸展分为三种操作,Zig,Zig-Zig和Zig-Zag。
- 如果 R R R就是 X X X的祖父节点,那么我们直接一次旋转 X X X即可达成目的,这种情况叫做Zig。
- 如果 X Y Z XYZ XYZ三者共线,那么我们先旋转 Y Y Y,再旋转 X X X。这种情况叫做ZIg-Zag。是双旋操作。
- 如果 X Y Z XYZ XYZ三者不共线,那么我们先旋转 X X X,再旋转 X X X。这种情况叫做ZIg-Zig。也是是双旋操作。
为什么要三者共线的时候要先旋转 Y Y Y再旋转 X X X,这种双旋转结构可以降低树的层数。在 H > 4 H>4 H>4的时候才能看出来效果。具体参考:
那我们就可以写出代码。
void splay(int x, int r)
{
int y;
while ((y = tree[x].fa) != r) // 如果y不是目标节点
{
int z = tree[y].fa; // 获取祖父节点
if (z != r) // 如果z是不是目标节点
{
// 执行双旋转
if (tree[z].l == y && tree[y].l == x || tree[z].r == y && tree[y].r == x)
rotate(y);
else
rotate(x);
rotate(x);
}
else
rotate(x); // 直接单旋即可
}
if (r == 0)
root = x;
}
其他操作
其他查询、插入、删除等操作和二叉搜索树完全一直,只不过在找到目标节点之后,要将该节点Splay一下。
Splay与其他二叉搜索树不同的是Splay操作,可以把任意一个节点接到其子节点上,通常用两边夹的方法筛选节点。