伸展树(splay tree)是一种能自我调整的二叉搜索树(BST)。虽然某一次的访问操作所花费的时间比较长,但是平摊(amortized) 之后的访问操作(例如旋转)时间能达到O(logn)的复杂度。对于某一个被访问的节点,在接下来的一段时间内再次频繁访问它(90%的情况下是这样的,即符合90-10规则,类似于CPU内或磁盘的cache设计原理)的应用模式来说,伸展树是一种很理想的数据结构。这是因为最近被访问的节点一直位于根节点的附近,从而再次被访问时的搜索路径长度比较小。这点与平衡的二叉树(比如AVL树和红黑树)不一样。另外一点与其他平衡二叉树的区别是,伸展树不需要存储任何像AVL树中平衡因子(balance factor)那样的平衡信息,可以节省空间的开销。
但是伸展树也有自己的缺点,比如不像其他平衡二叉树那样即使最坏情况下也能达到O(logn)访问时间,它的最坏情况下只有O(n),跟单向链表一样。另外,伸展树的查找操作会修改树的结构,这与普通意义上的查找为只读操作习惯不太一样。
伸展树的实现有两种方式,一是自底向上(bottom-up),另外一种是自顶向下(top-down)。考虑到实现的难易程度,自顶向下的实现方式比较简单,因为自底向上需要保存已经被访问的节点,而自顶向下可以在搜索的过程中同时完成splay操作。两者得出的树结构可能不太一样,但是他们的平摊时间复杂度都是O(logn)。两种实现的基本操作就是splay,splay将最后被访问到的节点提升为根节点。splay具体操作过程是,在一般情况下每次考虑两级节点(目标节点的父节点和祖父节点),按照目标节点和父节点的各自所处的位置是它们各自的父节点的左孩子还是右孩子,可以分为:zig或zag,zig-zig或zag-zag,zig-zag或zag-zig六种,其中后一种是前一种的对称形式。zig表示某节点(可以为目标节点或父节点)是它的父节点的左孩子;zag表示某节点(可以为目标节点或父节点)是它的父节点的右孩子。
在自顶向下(top-down)的实现中,需要将输入的树拆成三颗树,分别为左树L,中树M和右树R。其中M树维护当前还未被访问到的节点,L树中所有节点的值都小于M树中的任何节点值,R树中所有节点的值都大于M树中的任何节点值。L树中只需要知道当前的最大节点 (leftMax),而R树中只需要知道当前的最小节点(rightMin)。左右两棵树的根节点分别可以通过pseudoNode节点(它是leftMax和rightMin的初始值,而且splay过程中变量pseudoNode本身未变化,只改变它的左右孩子节点)的右和左孩子节点得到,因为leftMax中加入一个新的节点或子树时都是将新的节点作为leftMax的右孩子,而不是左孩子(注意这里的顺序),rightMin跟leftMax相反。自顶向下的zig-zig或zag-zag需要做旋转操作,zig-zig的旋转操作叫rotateLeftChild,旋转后目标节点的父节点和祖父节点加入R树,zag-zag的旋转操作叫rotateRightChild,旋转后目标节点的父节点和祖父节点加入L树。另外zig-zag或zag-zig可以分别简化为zig或zag操作,这样可以将zig-zag和zig合二为一,从而只需考虑一种情况,而不需要将两种情况单独考虑。zig操作将目标节点的父节点加入R树,zag操作将目标节点的父节点加入L树。注意L和R树中每次加入新节点都需更新变量leftMax或rightMin。自顶向下splay操作的最后一步是重组(re-assemble):将M树的左孩子设置为L树的根节点,将M树的右孩子设置为R树的根节点,然后M树原来的左孩子成为leftMax的右孩子,M树原来的右孩子成为rightMin的左孩子。
伸展树的基本操作及其实现:
1)查找boolean find(int x):查找操作只需要splay最后被访问的节点。在splay操作中,如果目标值x比最后一个被查找的叶子节点小,表示未找到该值,则splay该叶子节点作为新树的根节点,返回false;如果目标值x比最后一个被查找的叶子节点大,也表示未找到该值,则splay该叶子节点作为新树的根节点,返回false。如果目标值x跟某个节点值匹配,直接splay该节点作为新树的根节点,返回true。
2)找最大和最小值int findMax()和int findMin():findMax操作只需要将splay的目标值设定为整数最大值Integer.MAX_VALUE,就可以将树中的max值splay到根节点。findMin类似。
3)删除最大和最小值int deleteMax()和int deleteMin():deleteMax调用findMax()之后,这时最大值位于根节点而且根节点没有右子树(因为所有的值都比max小),只需删除根节点,然后将根节点的左子树设置成新的根节点。deleteMin类似。有了这两个操作和下面的insert操作,splay树可以作为优先队列(priority queue)使用, 它的平摊时间复杂度与用堆(完全二叉树)实现的优先队列相当。
4)插入新节点void insert(int x):目的是插入完成后将x节点变成根节点。另外插入操作不允许原树中已经有相同值x的节点。首先splay(x),将最后被访问的节点变为根节点,如果x跟root节点的值一样,说明重复插入;如果x比root节点的值小,但是由于 splay(x)之后,x值肯定比root左子树中的任何节点值都大,因为root节点是比x大的节点中最小的,这时可以将splay后的树拆成两颗子树,左边为根节点的左孩子作为左子树的root,右边为splay后的树的根节点作为右子树的根节点,然后将x设为root,它的左孩子为拆分出的左子树,它的右孩子为拆分出的右子树;如果x比root节点的值大,做法类似。
5)删除已经存在的节点void remove(int x):删除操作不允许原树中不存在具有相同值x的节点。首先splay(x),将最后被访问的节点变为根节点,如果x跟root节点的值不一样,说明原树中不存在x值的节点;如果root的左子树为空,说明x是最小节点,可以直接删除root,然后将root的右孩子作为新的根节点;如果root的左子树不为空,可以通过findMax的一个重载方法Node findMax (Node rootNode) 只找左子树的最大值(查找的过程同时splay左子树的最大值作为该子树的根节点),然后左子树最大值节点作为新树的根节点,删除原树的根节点,将新树根节点的右孩子设置为原树根节点的右孩子。
以下是使用自顶向下(top-down)方式实现伸展树及其基本操作。
测试:
Test case 1 - splay opeartion:
Before being splayed, in-order BST:
5 12 13 15 16 18 20 24 25 30
*****splay the node with value=19*****
After being splayed, in-order BST:
5 12 13 15 16 18 20 24 25 30
After being splayed, the tree is:
18
12 20
5 15 25
13 16 24 30
************************************
Test case 2 - splaytree operations:
Before being splayed, in-order BST:
10 12 13 20 25 29 35
*****25 is in the tree? [true]*****
After being splayed, in-order BST:
10 12 13 20 25 29 35
After being splayed, the tree is:
25
13 35
10 20 29
12
*****max value=35*****
After being splayed, in-order BST:
10 12 13 20 25 29 35
After being splayed, the tree is:
35
25
13 29
10 20
12
*****min value=10*****
After being splayed, in-order BST:
10 12 13 20 25 29 35
After being splayed, the tree is:
10
25
13 35
12 20 29
*****deleted max value: 35*****
After being splayed, in-order BST:
10 12 13 20 25 29
After being splayed, the tree is:
25
10 29
13
12 20
*****deleted min value: 10*****
After being splayed, in-order BST:
12 13 20 25 29
After being splayed, the tree is:
25
13 29
12 20
*****insert new value 24*****
After being splayed, in-order BST:
12 13 20 24 25 29
After being splayed, the tree is:
24
20 25
13 29
12
*****remove value 12*****
After being splayed, in-order BST:
13 20 24 25 29
After being splayed, the tree is:
20
13 24
25
29
************************************
Test case 3 - priority queue:
Before being splayed, in-order BST:
1 4 7 9 20 22 26 29 30 36
*****deleted max value: 36*****
After being splayed, in-order BST:
1 4 7 9 20 22 26 29 30
After being splayed, the tree is:
4
1 9
7 22
20 29
26 30
*****deleted max value: 30*****
After being splayed, in-order BST:
1 4 7 9 20 22 26 29
After being splayed, the tree is:
9
4 29
1 7 22
20 26
*****deleted max value: 29*****
After being splayed, in-order BST:
1 4 7 9 20 22 26
After being splayed, the tree is:
9
4 22
1 7 20 26
*****deleted max value: 26*****
After being splayed, in-order BST:
1 4 7 9 20 22
After being splayed, the tree is:
22
9
4 20
1 7
*****insert new value 16*****
After being splayed, in-order BST:
1 4 7 9 16 20 22
After being splayed, the tree is:
16
9 20
4 22
1 7
*****deleted max value: 22*****
After being splayed, in-order BST:
1 4 7 9 16 20
After being splayed, the tree is:
20
16
9
4
1 7
*****deleted max value: 20*****
After being splayed, in-order BST:
1 4 7 9 16
After being splayed, the tree is:
16
9
4
1 7
*****insert new value 12*****
After being splayed, in-order BST:
1 4 7 9 12 16
After being splayed, the tree is:
12
9 16
4
1 7
*****deleted max value: 16*****
After being splayed, in-order BST:
1 4 7 9 12
After being splayed, the tree is:
12
9
4
1 7
参考资料:
1. "Self-adjusting Binary Search Trees", Sleator and Tarjan (1985)
2. A demonstration of top-down splaying (http://www.link.cs.cmu.edu/splay/)