自顶而下伸展树的定义
伸展树是一棵二叉搜索树,但不保存每个节点的平衡信息,也没有特定的规则来保证伸展树的平衡性,而是在维护操作中通过将待维护的节点伸展到根来维持平衡。由于笔者在本文中实现的是自顶而下伸展树,故树上的每个节点不保存其父节点,只保存左右子节点和权值以提高效率。
对于自顶而下伸展树的定义代码如下:
#define NIL 0
#define ST_ROOT STree[NIL].left
struct SPLAY_TREE_NODE
{
int key,left,right;
}STree[MAX_N];
伸展树自顶而下的单元素伸展维护
若是伸展树自底而上的实现,则伸展操作无异于通过一系列的旋转操作,将待伸展节点旋转到根节点并返回。但是,由于每个节点未保存其父节点,故自顶而下的伸展不能也无需这样做。同样,由于自顶而下伸展树未保存父节点的原因,故笔者在其代码实现中也省去了不必要的旋转操作,而是使用一种称之为半伸展(semisplay)的方法保证伸展操作的正确性。
我们通过在向下寻找待伸展节点的过程中,将原树划分成三个部分,分别记为 LeftTree、MiddleTree 和 RightTree,其中,LeftTree 存储所有位于当前查找点上方,且权值小于待伸展节点权值的点所构成的重构树,MiddleTree 存储以当前查找点为根的子树,RightTree 存储所有位于当前查找点上方,且权值大于待伸展节点权值的点所构成的重构树。当当前查找点等于待伸展节点时,整个划分过程结束。
每次划分中,若待伸展节点位于当前查找点左子树,则将当前查找点及其右子树从 MiddleTree 并入 RightTree 中;若待伸展节点位于当前查找点右子树,则将当前查找点及其左子树从 MiddleTree 并入 LeftTree 中。在自顶而下构建 LeftTree 的过程中,我们可以发现,先并入节点的权值一定小于后并入节点的权值;同样,RightTree 中先并入节点的权值一定大于后并入节点的权值。
这样,我们记录下 LeftTree 中权值最大的点 LeftTreeMax,可知 LeftTreeMax 的右子节点一定为空(否则不满足中序遍历有序),而将待并入节点及其左子树并入 LeftTreeMax 的右子节点,更新 LeftTreeMax 完成 LeftTree 的重构(此时新 LeftTreeMax 即为待并入节点,其原右子树为新 MiddleTree,划分后右子节点为空);类似的,我们记录下 RightTree 中权值最小的点 RightTreeMin,可知 RightTreeMin 的左子节点一定为空(否则不满足中序遍历有序),而将待并入节点及其右子树并入 RightTreeMin 的左子节点,更新 RightTreeMin 完成 RightTree 的重构(此时新 RightTreeMin 即为待并入节点,其原左子树为新 MiddleTree,划分后左子节点为空)。每次将当前查找点下移,直至到达待伸展节点。
在最后的合并中,我们将待伸展节点的左子树并入 LeftTreeMax 的右子节点,而将待伸展节点的右子树并入 RightTreeMin 的左子节点,并将整个 LeftTree 并入待伸展节点的左子节点,将整个 RightTree 并入待伸展节点的右子节点,完成对整个树的重构。显然,重构后的树满足其中序遍历与原树的中序遍历相同,且待伸展节点位于重构树的根。这样即可无旋转地完成伸展树自顶而下的伸展操作。
对于伸展树自顶而下的单元素伸展维护代码如下:
int ST_Splay(int now,int key)
{
int Header=ST_NewNode(),LeftTreeMax=Header,RightTreeMin=Header;
while(key!=STree[now].key)
if(key<STree[now].key)
{
if(STree[now].left==NIL)
break;
STree[RightTreeMin].left=now;
RightTreeMin=now;
now=STree[now].left;
}
else
{
if(STree[now].right==NIL)
break;
STree[LeftTreeMax].right=now;
LeftTreeMax=now;
now=STree[now].right;
}
STree[LeftTreeMax].right=STree[now].left;
STree[RightTreeMin].left=STree[now].right;
STree[now].left=STree[Header].right;
STree[now].right=STree[Header].left;
ST_DeleteNode(Header);
return now;
}
伸展树自顶而下的单元素插入维护
单元素插入时,将与待插入元素权值最相近节点伸展到根,当待插入元素小于根权值时,将根的左子树并入待插入元素的左子节点,将根及其右子树并入待插入元素的右子节点,更新伸展树的根即可。若待插入元素大于根权值时同理。
对于伸展树自顶而下的单元素插入维护代码如下:
int ST_Memory[MAX_N],ST_MemTop=0;
int ST_NewNode()
{
if(ST_Memory[0]>0)
return ST_Memory[ST_Memory[0]--];
return ++ST_MemTop;
}
void ST_Insert(int key)
{
int now=ST_NewNode();
STree[now].left=STree[now].right=NIL;
STree[now].key=key;
if(ST_ROOT!=NIL)
{
ST_ROOT=ST_Splay(ST_ROOT,key);
if(key<STree[ST_ROOT].key)
STree[now].left=STree[ST_ROOT].left,
STree[now].right=ST_ROOT,
STree[ST_ROOT].left=NIL;
else
STree[now].left=ST_ROOT,
STree[now].right=STree[ST_ROOT].right,
STree[ST_ROOT].right=NIL;
}
ST_ROOT=now;
}
伸展树自顶而下的单元素删除维护
单元素删除时,将待删除元素伸展到根,将根的右子树中与待删除元素权值最相近节点,即最小权值节点伸展到根的右子节点,此时根的右子节点必然无左子树,将根的左子树并入根的右子节点的左子节点中,删除根节点,并更新伸展树的根为原根的右子节点即可。
对于伸展树自顶而下的单元素删除维护代码如下:
void ST_DeleteNode(int pos)
{
STree[pos].key=STree[pos].left=STree[pos].right=NIL;
ST_Memory[++ST_Memory[0]]=pos;
}
void ST_Delete(int key)
{
int del;
if(ST_ROOT!=NIL)
{
ST_ROOT=ST_Splay(ST_ROOT,key);
if(key==STree[ST_ROOT].key)
{
del=ST_ROOT;
if(STree[ST_ROOT].left!=NIL)
{
if(STree[ST_ROOT].right!=NIL)
STree[ST_ROOT].left=ST_Splay(STree[ST_ROOT].left,key+1),
STree[STree[ST_ROOT].left].right=STree[ST_ROOT].right;
ST_ROOT=STree[ST_ROOT].left;
}
else
ST_ROOT=STree[ST_ROOT].right;
ST_DeleteNode(del);
}
}
}
自顶而下伸展树的总结
虽然伸展树编程思想较为简单,总摊还时间复杂度较低,但不能保证单次操作时间复杂度,故在比较正式的 STL 模板中,二叉平衡树多用时间效率较为稳定的红黑树实现。相对于原版自底而上伸展树而言,笔者实现的无旋转的自顶而下伸展树相当于一个变种,在一字型情况的旋转中略有差异,而在之字形情况的旋转中等价。在 Wikipedia 有关于 Splay tree 中对自底而上伸展树有很好的介绍和代码实现,在这里笔者不在赘述。