1、Splay树及其应用,Yali 朱全民,二叉查找树,二叉查找树(Binary Search Tree) 可以被用来表示有序集合、建立索引或优先队列等。最坏情况下,作用于二叉查找树上的基本操作的时间复杂度,可能达到O(n)。某些二叉查找树的变形,基本操作在最坏情况下性能依然很好,如红黑树、AVL树等。,Splay 树,Splay树是二叉查找树的改进。 对Splay树的操作的均摊复杂度是O(log2n)。 Splay树与二叉查找树一样,也具有有序性。即Splay树中的每一个节点x都满足:该节点左子树中的每一个元素都小于x,而其右子树中的每一个元素都大于x。Splay树的核心思想就是通过Splay操
2、作进行自我调整,从而获得平摊较低的时间复杂度。,Splay操作,Splay操作是在保持Splay树有序性的前提下,通过一系列旋转操作将树中的元素x调整至树的根部的操作(Zig:右旋,Zag:左旋)。在旋转的过程中,要分三种情况分别处理:1)Zig 或 Zag2)Zig-Zig 或 Zag-Zag3)Zig-Zag 或 Zag-Zig,Splay操作 情况1,Zig或Zag操作: 节点x的父节点y是根节点。,Splay操作 情况2,Zig-Zig或Zag-Zag操作:节点x的父节点y不是根节点,且x与y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。,Spaly操作 情况3,Zig-Zag
3、或Zag-Zig操作:节点x的父节点y不是根节点,x与y中一个是其父节点的左孩子而另一个是其父节点的右孩子。,Splay操作举例,Splay(1,S),Spaly树基本操作,查找:与二叉排序树查找类似,只是查找结束后要将找到的元素通过Splay操作旋转到根。插入:用查找过程找到要插入的位置,进行插入。然后将新元素旋转到根。删除:首先在树中找到要删除的元素,将他转到根节点并删去,这样原来的树就分裂成了两棵树,然后将左子树中拥有最大关键字的那一个元素转到根,由于它是左子树中的最大元素,所以他不存在有儿子,这样只要把原来的右子树作为他的右子树,就重新合并成了一棵树。可见在Splay树的基本操作中,处
4、处要用到Splay旋转操作!,例一 Pet(湖南省省队选拔赛),宠物收养场提供两种服务:收养被离弃的宠物与让新的主人领养宠物。每个宠物有不相同的特点值。领养人所希望的特点值也不相同。如果领养人/遗弃宠物过多,则当前来的宠物/领养人选择离其特点值最近的(如果有两个特点值与其同样接近,则选择较小的)领养人/宠物,被领养/领养。并且纪录两个特点值的差值。 输入N条请求(X,Y),X=0表示宠物;X=1表示领养人,Y为特征值。 任务是计算所有差值的和。 (N=80000,等待的宠物/领养者数M=10000),例一 Pet,我们先用最普通的数组记录过多的宠物/领养人。 查找:需要检索整个数组,复杂度为O
5、(M) 删除:删除指定元素之后,需要成批移动主轴元素,时间复杂度也为O(M)这样最坏情况下时间复杂度为O(NM), 是不可取的。,例一 Pet,对宠物/领养人过多的情况下,我们建立一颗排序二叉树,并记录其属性Sign(宠物/领养人)。 对于命令(X,Y),如果树为空或者X=Sign,则将其插入,并令Sign=x; 否则在树中查找符合要求的结点,记录特征值之差,并将其删除。(注意无论树的属性是0或1,相对进行的操作是相同的) 普通的排序二叉树在最坏情况下时间复杂度也是O(NM) 。我们可以应用较高效的排序二叉树,例如AVL树(平衡二叉树)或者Splay树。,例一 Pet,AVL树引入平衡因子的概
6、念,使得整棵排序二叉树严格平衡,时间复杂度严格为O(nlogm)。但是其编程复杂度很高。Splay树通过Splay旋转操作,使得分摊时间复杂度为O(nlogm),虽然常系数较AVL树相比大。但是操作简便,编程复杂度较低。在考场上我们要综合考虑各方面的因素,合理的选择数据结构。,例二 郁闷的出纳员(NOI2004),你是一个公司出纳员,需要处理n条下面的命令:此外,如果某个员工的工资低于最低工资下界Min,他就会立刻离开公司。 现在请你写一个程序完成这个工资统计的工作。,例二 郁闷的出纳员,这个题目简单来说就是请你设计一种数据结构满足如下4种操作: 向集合中插入一个数; 将集合中所有的数都加上一
7、个值; 将集合中所有的数都减去一个值,并将所有低于min的数从集合中删除掉(min是事先给定的一个固定的数); 查找集合中第k大的数。我们考虑应用Splay树维护这个工资系统。,例二 郁闷的出纳员,Splay树的插入、删除、查找第K大元素都可以在均摊时间复杂度O(logn)内完成。但是还有一种操作就是将集合内的所有元素都加上或减去一个值,如果单纯的按照题目要求对数据进行改动,则每一步这样的操作所需的时间复杂度为O(NlogN)(因为需要重建二叉树),例二 郁闷的出纳员,注意到,这个增值操作不会改变已经在树中的元素的大小关系,只会改变已经在集合内的元素与以后将要加进来的元素间的关系。这样我们就可
8、以只改变以后要加进来的元素而不改变当前已有的元素。 具体做法为设立一个表示改变量的变量delta,遇到改值操作,只需令delta = delta + a,同时将Splay树中所有小于min-delta的元素删掉。当新增一个值为x的元素时,只需先修改为x-delta在插入树中即可。这样改值的复杂度就为O(1)了,例二 郁闷的出纳员,通过Splay树维护工资系统,我们得到了时间复杂度为O(nlogn)的算法。至此,问题得到解决。,例三 序列终结者,给定一个长度为N的序列,每个序列的元素是一个整数。要支持以下三种操作: 将L,R这个区间内的所有数加上V。 将L,R这个区间翻转,比如1 2 3 4变成
9、4 3 2 1。 求L,R这个区间中的最大值。 最开始所有元素都是0。 N=50000,操作数=100000。,例三 序列终结者,我们将序列的每个元素值作为关键字,将它们在序列中的相对位置作为判定大小关系的函数(左小右大),这样就可以建立一棵n个节点的Splay树。 我们给Splay树增添三个域: Treev.Add:记录以节点v为根的子树被整体改值的情况。 Treev.Turn:记录以节点v为根的子树所表示的这段区间有没有被整体翻转过。 Treev.Sum:记录以节点v为根的子树中的最大值。三个序列操作都和某一段给定区间L,R有关,所以我们首先介绍区间的截取操作。,例三 序列终结者,首先我们
10、找到Splay中的第L大的元素,将它旋转到根,将并将它的左子树分离出来。设原树是T1,它的左子树是T2 则现在的Splay树T2就是序列1,L-1所代表的树;T1就是序列L,n所代表的树。 同理,找到T1中第R-L+1大的元素,将它旋转到根,并将T1的右子树分离出来,设为T3。 则现在T1就是序列L,R所代表的树,T3就是序列R+1,n所代表的树。,例三 序列终结者,这样我们就成功分离出L,R区间,并且在效率上相当于只用了常数次的查找操作。 这是我们只需要对T1的根节点进行修改Sum、Add或者Turn域的修改即可。 修改完后,再将T1、T2、T3进行合并,易知合并在效率上也相当于常数次的查找
11、操作。所以对于序列的每一种操作,都可以在均摊复杂度O(logn)完成。,例三 序列终结者,现在的问题是,修改了某一个子树的Sum、Add、Turn值之后,以后在进行查找、旋转等操作的时候怎样处理和运用?我们可以在查找到某个节点v的时候,将它的Sum、Add、Turn值传递到子树上,这样就不影响后面的操作了。,例三 序列终结者,这里以修改Add为例。当查找到节点p时,进行如下操作: p.datap.data+p.add; p.lch.addp.lch.add+p.add; p.rch.addp.rch.add+p.add; /将p节点的add值 分散到左右子树和p节点的data中 p.add0; /清空add域Sum、Turn域的修改也可以类似进行。,例三 序列终结者,综合上面的分析,我们可以在O(mlogn)的时间复杂度内解决序列操作的问题。至此问题得到解决。,