之前也写过两篇关于伸展树的,一篇是概念,一篇是实现。
今天重温一下。
回顾往昔,光阴似箭,日月如梭啊。
现在我们来介绍一种相对与AVL树更简单的数据结构,它叫伸展树,它保证从空树开始连续任意M次操作最多花费O(MlogN)时间。虽然这种保证并不排除任一次操作花费的时间为O(N),但是我们关注的是最终的结果。
伸展树是基于这样的事实:对于二叉查找树来说,每次操作的最坏时间为O(N),并不坏,只要它不时常发生就行。
伸展树的基本想法是:考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当经常处于靠近树根的位置。这样,很容易得想到以下这个方案:每次查找节点之后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操作后,它均会通过旋转的方法把被访问节点旋转到树根的位置。
旋转方式参见为实习准备的数据结构(5)-- 图解AVL树(平衡二叉搜索树)
实施上面描述的重新构造的一种方法是执行单旋转,这意味着我们将在访问路径上的每一个节点和它们的父节点进行旋转。
作为栗子,考虑在下面的树中对k1进行一次访问之后所发生的情况:
一次旋转之后:
k1还没到根,继续转:
转,再转:
好,转完了。
可以看到,本来一棵长树变成了近乎平衡的树。
这些旋转的效果是将k1不断推向树根,使得k1的进一步访问很容易(没被再推走之前)。
不过缺点也很明显,原先的带头大哥k3,百分之九十九也是刚刚坐上头把交椅,屁股还没坐热就让k1给踢到小角落去了。
展开的思路类似于前面介绍的旋转的想法,不过在旋转如何实施上我们稍微有一点选择的余地。我们仍然从底部向上沿着访问路径旋转。
令X是在访问路径上的一个非根节点,我们将在这个路径上实施旋转操作。如果X的父节点是根节点,那么我们只需要旋转X和树根。否则,X就有父亲§和祖父(G),存在以上两种情况以及对称的情形要考虑(跟AVL树差不多)。
也就是AVL树里那俩要双旋的。
也就是AVL树里那俩只需要单旋的。
注意甄别这次旋转和之前旋转的不同,更要看清楚和标准AVL单旋的差别。
这一次一字型旋转,其中包含了两次的AVL单旋。
首先,对P做一次单旋
然后,对X做一次单旋
来看个栗子,还是上面那个,k1
展开的第一步是在k1,显然是一个之字型,因此我们用k1,k2,k3执行一次标准的AVL双旋转,得到如下的树:
注意和上面的旋转进行比对。
这一步转完之后,迎接k1的是一个一字型,因此我们用k1,k4,k5来做一次一字型旋转,注意看:
虽然从一些小栗子上很难看出来,但是展开操作不仅将访问节点移动到根处,而且还把访问路径上的大部分节点深度大致减少一半的效果(某些浅的节点最多向后推两个层次)。
这个删除略微抽象了一点,简而言之,就是:
上面的操作,需要一次自顶向下的一次遍历,而后自底向上的一次遍历。这可以通过备忘录模式来实现(自底向上需要沿途保存节点),不过这需要大量的开销,而且也需要处理许多特殊情况。那么,接下来我们来讲一下如何在初始访问路径上施行一些旋转,结果得到在实践中更快的过程,只用到O(1)的额外空间,但却保持了O(logN)的摊还时间界。
为了叙述的方便,上图的右旋叫做X绕Y右旋,左旋叫做Y绕X左旋。
当我们沿着树向下搜索某个节点X的时候,我们将搜索路径上的节点及其子树移走。我们构建两棵临时的树──左树和右树。没有被移走的节点构成的树称作中树。在伸展操作的过程中:
1、当前节点X是中树的根。
2、左树L保存小于X的节点。
3、右树R保存大于X的节点。
开始时候,X是树T的根,左右树L和R都是空的。
和自底向上一样,自顶向下也分了三种情况。
如上图,在搜索到X的时候,所查找的节点比X小,将Y旋转到中树的树根。旋转之后,X及其右子树被移动到右树上。很显然,右树上的节点都大于所要查找的节点。注意X被放置在右树的最小的位置,也就是X及其子树比原先的右树中所有的节点都要小。这是由于越是在路径前面被移动到右树的节点,其值越大。读者可以分析一下树的结构,原因很简单。(就这句,给我点醒了)
通了一点之后,后面就好办了。
在这种情况下,所查找的节点在Z的子树中,也就是,所查找的节点比X和Y都小。所以要将X,Y及其右子树都移动到右树中。首先是Y绕X右旋,然后Z绕Y右旋,最后将Z的右子树(此时Z的右子节点为Y)移动到右树中。注意右树中挂载点的位置。
在这种情况中,首先将Y右旋到根。这和Zig的情况是一样的。然后变成上图右边所示的形状。接着,对Z进行左旋,将Y及其左子树移动到左树上。这样,这种情况就被分成了两个Zig情况。这样,在编程的时候就会简化,但是操作的数目增加(相当于两次Zig情况)。
将中树的左右子树分别连接到左树的右子树和右树的左子树上。将左右树作为X的左右子树。重新最成了一所查找的节点为根的树。
下面是一个查找节点19的例子:
在例子中,树中并没有节点19,最后,距离节点最近的节点18被旋转到了根作为新的根。节点20也是距离节点19最近的节点,但是节点20没有成为新根,这和节点20在原来树中的位置有关系。
而一直困扰我的,就是第二步到第三步的转化,为什么要把20提上去,现在明白了。
#include
#include
int size; /* number of nodes in the tree */
/* Not actually needed for any of the operations */
typedef struct tree_node Tree;
struct tree_node
{
Tree *left, *right;
int item;
};
Tree *splay (int i, Tree *t)
{
/* Simple top down splay, not requiring i to be in the tree t. */
/* What it does is described above. */
Tree N, *l, *r, *y;
if (t == NULL)
return t;
N.left = N.right = NULL;
l = r = &N;
for (;;)
{
if (i < t->item)
{
if (t->left == NULL)
break;
if (i < t->left->item)
{
y = t->left; /* rotate right */
t->left = y->right;
y->right = t;
t = y;
if (t->left == NULL)
break;
}
r->left = t; /* link right */
r = t;
t = t->left;
}
else if (i > t->item)
{
if (t->right == NULL)
break;
if (i > t->right->item)
{
y = t->right; /* rotate left */
t->right = y->left;
y->left = t;
t = y;
if (t->right == NULL)
break;
}
l->right = t; /* link left */
l = t;
t = t->right;
}
else
break;
}
l->right = t->left; /* assemble */
r->left = t->right;
t->left = N.right;
t->right = N.left;
return t;
}
/* Here is how sedgewick would have written this. */
/* It does the same thing. */
Tree * sedgewickized_splay (int i, Tree * t)
{
Tree N, *l, *r, *y;
if (t == NULL)
return t;
N.left = N.right = NULL;
l = r = &N;
for (;;)
{
if (i < t->item)
{
if (t->left != NULL && i < t->left->item)
{
y = t->left;
t->left = y->right;
y->right = t;
t = y;
}
if (t->left == NULL)
break;
r->left = t;
r = t;
t = t->left;
}
else if (i > t->item)
{
if (t->right != NULL && i > t->right->item)
{
y = t->right;
t->right = y->left;
y->left = t;
t = y;
}
if (t->right == NULL)
break;
l->right = t;
l = t;
t = t->right;
}
else
break;
}
}
l->right=t->left;
r->left=t->right;
t->left=N.right;
t->right=N.left;
return t;
}
Tree * insert(int i, Tree * t)
{
/* Insert i into the tree t, unless it's already there. */
/* Return a pointer to the resulting tree. */
Tree * new_node;
new_node = (Tree *) malloc (sizeof (Tree));
if (new_node == NULL)
{
printf("Ran out of space\n");
exit(1);
}
new_node ->item = i;
if (t == NULL)
{
new_node->left = new_node->right = NULL;
size = 1;
return new_node;
}
t = splay(i,t);
if (i < t->item)
{
new_node->left = t->left;
new_node->right = t;
t->left = NULL;
size ++;
return new_node;
}
else if (i > t->item)
{
new_node->right = t->right;
new_node->left = t;
t->right = NULL;
size++;
return new_node;
}
else
{
/* We get here if it's already in the tree */
/* Don't add it again */
free(new_node);
return t;
}
}
Tree * delete(int i, Tree * t)
{
/* Deletes i from the tree if it's there. */
/* Return a pointer to the resulting tree. */
Tree * x;
if (t==NULL)
return NULL;
t = splay(i,t);
if (i == t->item)
{
/* found it */
if (t->left == NULL)
x = t->right;
else
{
x = splay(i, t->left);
x->right = t->right;
}
size--;
free(t);
return x;
}
return t; /* It wasn't there */
}
int main(int argv, char *argc[])
{
/* A sample use of these functions. Start with the empty tree, */
/* insert some stuff into it, and then delete it */
Tree * root;
int i;
root = NULL; /* the empty tree */
size = 0;
for (i = 0; i < 1024; i++)
{
root = insert((541*i) & (1023), root);
}
printf("size = %d\n", size);
for (i = 0; i < 1024; i++)
{
root = delete((541*i) &(1023), root);
}
printf("size = %d\n", size);
}