参考:邓俊辉 的数据结构,图片来自该资料
伸展树(SplayTree)
相对于AVL,Splay的实现更为简捷。伸展树无需时刻都严格地保持全树的平衡,但却能够在任何足够长的真实操作序列中,保持分摊意义上的高效率。伸展树也不需要对基本的二叉树节点结构,做任何附加的要求或改动,更不需要记录平衡因子或高度之类的额外信息,故适用范围更广。
通常在任意数据结构的生命期内,执行不同操作的概率往往极不均衡,而且各操作之间具有极强的相关性,并在整体上多呈现出极强的规律性。其中最为典型的,就是所谓的“数据局部性”(data locality),这包括两个方面的含义:
- 刚刚被访问过的元素,极有可能在不久之后再次被访问到
- 将被访问的下一元素,极有可能就处于不久之前被访问过的某个元素的附近
如果将该策略应用于二叉搜索树。只需将刚被访问的节点,及时地“转移”至树根(附近),即可加速后续的操作。当然, 转移前后的搜索树必须相互等价,故为此使用前文介绍的“旋转“等价变换的技巧。
逐层伸展
每访问过一个节点之后,随即反复地以它的父节点为轴,经适当的旋转将其提升一层,直至最终成为树根。以下图为例,若深度为3的节点E刚被访问–无论查找或插入,甚至“删除”都可通过3次旋转,将该树等价变换为以E为根的另一棵二叉搜索树
随着节点E的逐层上升,两侧子树的结构也不断地调整,故这一过程也形象地称作伸展 (splaying),而采用这一调整策略的二叉搜索树也因此得名。不过,为实现真正意义上的伸 展树,还须对以上策略做点微妙而本质的改进。之所以必须改进,是因为目前的策略仍存在致命 的缺陷—对于很多访问序列,单次访问的分摊时间复杂度在极端情况下可能高达n。
不难验证,若从空树开始依次插入关键码{ 1, 2, 3, 4, 5 },且其间采用如上调整策略,
则可得到如下图所示的二叉搜索树。
在各次访问之后,为将对应节点伸展调整至树根,分别需做4、4、3、2和1次旋转。
一般地,若节点总数为n,则旋转操作的总次数应为:
(n - 1) + { (n - 1) + (n - 2) + … + 1 }= (n2 +n-2)/2 = O( n2)。
如此分摊下来,每次访问平均需要n时间。很遗憾,这一效率不仅远远低于AVL树,而且甚至与原始的二叉搜索树的最坏情况相当。
而事实上,问题还远不止于此。稍做比对即不难发现,上图a与f中二叉搜索树的结构完全相同。也就是说,经过以上连续的5次访问之后,全树的结构将会复原!
这就意味着,以上情况可以持续地再现。
当然,这一实例,完全可以推广至规模任意的二叉搜索树。于是对于规模为任意n的伸展树, 只要按关键码单调的次序,周期性地反复进行查找,则无论总的访问次数m >> n有多大,就分摊意义而言,每次访问都将需要O(n)时间!
双层伸展
为克服上述伸展调整策略的缺陷,一种简便且有效的方法就是:将逐层伸展改为双层伸展。 具体地,每次都从当前节点v向上追溯两层(而不是仅一层),并根据其父亲p以及祖父g的相对位置,进行相应的旋转。主要以下分三类情况:
zig-zig/zag-zag
如下图所示, 设v是p的左孩子,且p也是g的左孩子;
设W和X分别是v的左、右子树,Y和Z分别是p和g的右子树。
针对这种情况,首先以节点g为轴做顺时针旋转zig(g),其效果如图(b)所示。然后,再以p
为轴做顺时针旋转zig§,其效果如图©所示。如此连续的两次zig旋转,合称zig-zig调整。 自然地,另一完全对称的情形,v是p的右孩子,且p也是g的右孩子,则可通过连续的
两次逆时针旋转实现调整,合称zag-zag操作。
zig-zag/zag-zig
如下图所示,设v是p的左孩子,而p是g的右孩子;
设W是g的左子树,X和Y分别是v的左右子树,Z是p的右子树。
针对这种情况,首先以节点p为轴做顺时针旋转zig§,其效果如(b)所示。然后,再以g
为轴做逆时针旋转zag(g),其效果如图©所示。如此zig旋转再加zag旋转,合称zig-zag调整。 同样地,另一完全对称的情形–v是p的右孩子,而p是g的左孩子—则可通过zag旋转再加zig旋转实现调整,合称zag-zig操作。
zig/zag
如下图所示,若v最初的深度为奇数,则经过若干次双层调整至最后一次调整时,
v的父亲p即是树根r。
将v的左、右子树记作X和Y,节点p = r的另一子树记作Z。
此时,只需围绕p = r做顺时针旋转zig§,即 可如图(b)所示,使v最终攀升至树根,从而结束整个伸展调整的过程。
效果与效率
综合以上各种情况,每经过一次双层调整操作,节点v都会上升两层。若v的初始深度depth(v) 为偶数,则最终v将上升至树根。若depth(v)为奇数,则当v上升至深度为1时,不妨最后再相应 地做一次zig或zag单旋操作。无论如何,经过depth(v)次旋转后,v最终总能成为树根。
回顾最开始的单层伸展的例子中:在可持续重复的过程中,二叉搜索树的高度始终不小于n/2; 而且,至少有一半的节点在接受访问时,不仅没有如最初设想的那样靠近树根,而且反过来恰恰处于最底层。 从树高的角度看,问题根源也可再进一步地解释为:在持续访问的过程中,树高依算术级数逐步从n - 1递减至n/2,然后再逐步递增回到n - 1。
以如下图所示的二叉搜索树为例,在find(1)操作之后,采用逐层调整策略与双层调 整策略的效果,分别如图(a)和图©所示。
可见,最深节点(1)被访问之后再经过双层调整,不仅同样可将该节点伸展至树根,而且同时可使树的高度接近于减半。就树的形态而言,双层伸展策略可“智能”地“折叠”被访问的 子树分支,从而有效地避免对长分支的连续访问。这就意味着,即便节点v的深度为n,双层 伸展策略既可将v推至树根,亦可令对应分支的长度以几何级数(大致折半)的速度收缩。
伸展代码
/// <summary>
/// 伸展操作
/// Zig:右旋转,顺时针旋转
/// Zag:左旋转,逆时针旋转
///
/// - 单旋(单层伸展)
/// zig zag
/// (顺时针旋转g) (逆时针旋转g)
/// g g
/// / \ / \
/// p T3 T0 p
/// / \ / \
/// v T2 T1 v
/// / \ / \
/// T0 T1 T2 T3
///
/// zig单旋后: zag单旋后:
/// p p
/// / \ / \
/// v g g v
/// / \ / \ / \ / \
/// T0 T1 T2 T3 T0 T1 T2 T3
///
/// - 双旋(双层伸展)
/// zig-zig zig-zag zag-zag zag-zig
/// (先顺时针旋转g,后顺时针旋转p) (先顺时针旋转p(zig),后逆时针旋转g(zag)) (先逆时针旋转g,后逆时针旋转p) (先逆时针旋转p(zag),后顺时针旋转g(zig))
/// g g g g
/// / \ / \ / \ / \
/// p T3 T0 p T0 p p T3
/// / \ / \ / \ / \
/// v T2 v T3 T1 v T0 v
/// / \ / \ / \ / \
/// T0 T1 T1 T2 T2 T3 T1 T2
///
/// 双旋后: zig-zig 双旋后 zig-zag双旋后 zag-zag 双旋后 zag-zig双旋后
/// v v v v
/// / \ / \ / \ / \
/// T0 p g p p T3 p g
/// / \ / \ / \ / \ / \ / \
/// T1 g T0 T1 T2 T3 g T2 T0 T1 T2 T3
/// / \ / \
/// T2 T3 T0 T1
///
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
public Node Splay(Node v)
{
if (v==null) return null;
Node g=null, p=null;
while(!!(p=v.Parent)&& !!(g=p.Parent))//自下而上,反复对v做双层伸展
{
Node r = g.Parent;
if(IsLeft(v))
{
if(IsLeft(p)) //zig-zig 先旋转v的祖父节点,然后再旋转v的父节点
{
AttachAsLChild(g, p.R);AttachAsLChild(p, v.R);
AttachAsRChild(p, g);AttachAsRChild(v, p);
}
else //zig-zag 先旋转v的父节点,后旋转v的祖父节点
{
AttachAsLChild(p, v.R); AttachAsRChild(g, v.L);
AttachAsLChild(v, g); AttachAsRChild(v, p);
}
}
else
{
if (IsRight(p))// zag-zag
{
AttachAsRChild(g, p.L); AttachAsRChild(p, v.L);
AttachAsLChild(p, g); AttachAsLChild(v,p);
}
else //zag-zig
{
AttachAsRChild(p, v.L); AttachAsLChild(g, v.R);
AttachAsRChild(v, g); AttachAsLChild(v, p);
}
}
if (r == null)//若原v的曾祖父r不存在,则v现在为树根Root
{
v.Parent = null;
}
else
{
if (g == r.L)
AttachAsLChild(r, v);
else
AttachAsRChild(r, v);
}
UpdateHeight(g);
UpdateHeight(p);
UpdateHeight(v);
}//双层伸展结束时,必有g=null,但p可能非空
if(p==v.Parent&&p!=null) //如果p为非空,则需要做一次单旋
{
if(IsLeft(v)) /* zig */
{
AttachAsLChild(p, v.R);AttachAsRChild(v, p);
}
else /* zag */
{
AttachAsRChild(p, v.L); AttachAsLChild(v, p);
}
UpdateHeight(p);UpdateHeight(v);
}
v.Parent = null;
return v;
}
伸展树全部代码(C#)
伸展树测试
测试代码
static void Main()
{
SplayTree tree = new SplayTree();
Node n, n1, n2;
tree.Insert(20);
tree.Insert(10);
tree.Insert(7);
tree.Insert(24);
tree.Insert(26);
tree.Insert(12);
tree.Insert(18);
Console.Write(" Preorder::");
tree.Preorder(tree.Root);
Console.WriteLine();
Console.Write(" Inorder::");
tree.Inorder(tree.Root);
Console.WriteLine();
Console.Write(" Postorder::");
tree.Postorder(tree.Root);
Console.WriteLine();
Console.Write(" Levelorder::");
tree.Levelorder(tree.Root);
Console.WriteLine();
Console.Write(" ZLevelorder:");
tree.ZLevelorder(tree.Root);
Console.WriteLine("\n\n");
tree.Remove(24);
tree.Remove(20);
tree.Remove(10);
tree.Remove(18);
tree.Remove(7);
tree.Remove(26);
tree.Remove(16);
tree.Remove(12);
Console.ReadKey();
}
测试结果
Insert:20
020
Insert:10
010
/ \
N 020
Insert:7
007
/ \
N 010
/ \ / \
N N N 020
Insert:24
024
/ \
020 N
/ \ / \
010 N N N
/ \ / \ / \ / \
007 N N N N N N N
Insert:26
026
/ \
024 N
/ \ / \
020 N N N
/ \ / \ / \ / \
010 N N N N N N N
/ \ / \ / \ / \ / \ / \ / \ / \
007 N N N N N N N N N N N N N N N
Insert:12
012
/ \
010 026
/ \ / \
007 N 020 N
/ \ / \ / \ / \
N N N N N 024 N N
Insert:18
018
/ \
012 020
/ \ / \
010 N N 026
/ \ / \ / \ / \
007 N N N N N 024 N
Preorder::18,12,10,7,20,26,24,
Inorder::7,10,12,18,20,24,26,
Postorder::7,10,12,24,26,20,18,
Levelorder::18,12,20,10,26,7,24,
ZLevelorder:18,20,12,10,26,24,7,
Remove:24
026
/ \
018 N
/ \ / \
012 020 N N
/ \ / \ / \ / \
010 N N N N N N N
/ \ / \ / \ / \ / \ / \ / \ / \
007 N N N N N N N N N N N N N N N
Remove:20
026
/ \
018 N
/ \ / \
012 N N N
/ \ / \ / \ / \
010 N N N N N N N
/ \ / \ / \ / \ / \ / \ / \ / \
007 N N N N N N N N N N N N N N N
Remove:10
012
/ \
007 026
/ \ / \
N N 018 N
Remove:18
026
/ \
012 N
/ \ / \
007 N N N
Remove:7
012
/ \
N 026
Remove:26
012
Remove:16
012
Remove:12
N
可视化工具
可视化工具(旧金山大学 (usfca)|数据结构可视化工具)