除了拥有二叉查找树的性质之外,伸展树还具有的一个特点是:当某个节点被访问时,伸展树会通过旋转使该节点成为树根。这样做的好处是,下次要访问该节点时,能够迅速的访问到该节点。假设想要对一个二叉查找树执行一系列的查找操作。为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生,它是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

Splaying是Splay Tree中的基本操作,为了让被查询的条目更接近树根,Splay Tree使用了树的旋转操作,同时保证二叉排序树的性质不变。

Splaying的操作受以下三种因素影响:


  • 节点x是父节点p的左孩子还是右孩子
  • 节点p是不是根节点,如果不是
  • 节点p是父节点g的左孩子还是右孩子

同时有三种基本操作:


Zig Step

树形结构 伸展树_子树

当p为根节点时,进行zip step操作。

当x是p的左孩子时,对x右旋;

当x是p的右孩子时,对x左旋。


Zig-Zig Step

树形结构 伸展树_子树_02

当p不是根节点,且x和p同为左孩子或右孩子时进行Zig-Zig操作。

当x和p同为左孩子时,依次将p和x右旋;

当x和p同为右孩子时,依次将p和x左旋。



Zig-Zag Step

树形结构 伸展树_子树_03

当p不是根节点,且x和p不同为左孩子或右孩子时,进行Zig-Zag操作。

当p为左孩子,x为右孩子时,将x左旋后再右旋。

当p为右孩子,x为左孩子时,将x右旋后再左旋。

#include<stdio.h>

typedef struct SplayTreeNode {
int value;
node *father;
node *son[2];//0-left 1-right

SplayTreeNode(int v = 0, node * f = NULL)
{
value = v;
father = f;
son[0] = NULL;
son[1] = NULL;
}
}node;

SplayTreeNode *root;
bool which_son(SplayTreeNode *f, SplayTreeNode *s)//判断子节点在父节点左还是右
{
return f->son[1] == s;
}

void rotate(SplayTreeNode *tree)//函数会自行判断x实在父节点的左儿子上还是右儿子上,并自动左旋或右旋
{
SplayTreeNode *father = tree->father;
SplayTreeNode *grandf = father->father;
bool a = which_son(father, tree);
bool b = !a;
father->son[a] = tree->son[b];
if (tree->son[b] != NULL)
tree->son[b]->father = father;
tree->son[b] = father;
father->father = tree;
tree->father = grandf;
if (grandf != NULL)
grandf->son[which_son(grandf, father)] = tree;
else
root = tree;

}

//Splay(x, y)用于将x结点旋转到y结点的某个儿子上
void Splay(SplayTreeNode *t, SplayTreeNode *p)
{
while (t->father != p)
{
SplayTreeNode *father = t->father;
SplayTreeNode *grandf = father->father;
if (grandf == p)
rotate(t);
else
{
if (which_son(grandf, father) ^ which_son(father, t))
rotate(t), rotate(t);//^异或相同0,不同1
else
rotate(father), rotate(t);
}
}
}
//这里值得注意的是两种双旋。如果t(该节点),f(父亲节点),g(祖父节点)形成了一条单向的链,即[右→右]或[左→左]这样子,
//那么就先对父亲结点进行rotate操作,再对该节点进行rotate操作;否则就对该节点连续进行两次rotate操作。据称单旋无神犇,双旋o(logn),这句话我也没有考证,
//个人表示不想做什么太多的探究,毕竟splay的复杂度本来就挺玄学的了,而且专门卡单旋splay的题也没怎么听说过。
//对了,这个双旋操作和avl的双旋是不是有那么几分相似啊,虽然还是不太一样的吧,好吧其实也不怎么像╮(╯ - ╰)╭。

void insert(int val) {
if (root == NULL)
root = new node(val, NULL);
for (node *t = root; t; t = t->son[val >= t->value]) {
if (t->value == val) { Splay(t, NULL); return; }
if (t->son[val >= t->value] == NULL)
t->son[val >= t->value] = new node(val, t);
}
}

void erase(int val) {
node *t = root;
for (; t; ) {
if (t->value == val)
break;
t = t->son[val > t->value];
}
if (t != NULL) {
Splay(t, NULL);
if (t->son[0] == NULL) {
root = t->son[1];
if (root != NULL)
root->father = NULL;
}
else {
node *p = t->son[0];
while (p->son[1] != NULL)
p = p->son[1];
Splay(p, t); root = p;
root->father = NULL;
p->son[1] = t->son[1];
if (p->son[1] != NULL)
p->son[1]->father = p;
}
}
}
//与之对应的就是删除操作,相对的复杂一些。删除一个元素,需要先在树中找到这个结点,然后把这个结点Splay到根节点位置,开始分类讨论。
//如果这个结点没有左儿子(左子树),直接把右儿子放在根的位置上即可;
//否则的话就需要想方设法合并左右子树:在左子树种找到最靠右(最大)的结点,把它旋转到根节点的儿子上,此时它一定没有右儿子,
//因为根节点的左子树中不存在任何一个元素比它更大,那么把根节点的右子树接在这个结点的右儿子上即可。