平衡二叉查找树

二叉查找树是一种非常有用的数据结构。经过一定扩充,它可以支持的操作有:Insert(插入)、Find(查找)、Remove(删除)、GetMax(取最大)、GetMin(取最小)、Prev(取前驱)、Next(取后继)、Rank(取某元素的序号)、Select(按照序号取该元素)。如果数据是随机的话,以上操作可以保证在O(logn)的期望时间内完成。但是比赛中显然出题者会给出一些刁钻的数据让最普通的二叉查找树严重TLE,这便是平衡二叉树得以应用的时候了。平衡二叉树可以保证以上操作是严格(AVL)或均摊(Splay)或期望(Treap)的O(logn)的时间复杂度。

平衡二叉树的实现一般有三种情形:递归、Top-Down(自顶向下)、Buttom-Up(自底向上)。递归实现可能比较好写,比如Treap的递归实现就非常短,基本的插入删除二三十行就可以搞定。Splay的一种实现方法也是递归,但这种方法是不推荐使用的。因为Splay虽然保证均摊的时间复杂度,但并不保证高度(AVL保证高度是严格的O(logn)、Treap保证高度是期望O(logn)),这就造成面对某些刁钻的数据时虽时间上不会出问题,却可能由于递归层次过多造成栈溢出。(例如连续大量升序的插入会使Splay变成一条长链,此时访问最底端的节点就会造成栈溢出。)所以Splay比较好的编写方法还是Top-Down或者Buttom-Up,它们都是非递归的。其中Top-Down的核心Splay过程有大约三四十行,性能十分优异,虽然不太好理解但是强记住也是可取的。Buttom-Up的Splay我没写过,但我知道一切Buttom-Up的平衡树都需要维护每个节点的父指针,造成了一定的编程复杂度也容易出错(比如说最基本的旋转操作就比以前麻烦多了)。Treap也可以写成Buttom-Up,不过我觉得没必要——既然选择了Treap当然就是为了它的编程复杂度低,写Buttom-Up的Treap还不如写成Top-Down或Buttom-Up的Splay,没准行数还更少呢。但相对于递归实现以及Buttom-Up来说,Top-Down的缺点是很难维护每个节点的Size域(即这棵子树的节点数或者其左子树的节点数),事实上我一直认为Top-Down Splay是无法在均摊O(logn)的和不记录父指针的前提下加入维护size域的操作的——这两个前提若不满足则或失去了Splay的本质,或失去了Top-Down的精髓。综上所述呢,在考试时选择写哪种平衡树应该这样考虑:不需要Size且数据量极大时用Top-Down Splay,需要Size或数据量一般时用递归Treap,这两者我都有把握在15min内写对;Buttom-Up的唯一好处是既非递归又能维护Size,但似乎书写是最麻烦的,何况我没写过,所以就不考虑了。

下面说说平衡树的操作。Insert和Remove这种需要改变树中元素的操作因平衡方式而异,就不细说了。Find的基本方法就是不断比较待查找值与当前节点,若小于则向左,大于则向右,等于就找到了;Splay的Find有所不同,只要Splay这个值一下看看根部有没有就行了。 GetMax是从根一直向右走直至无法再走就找到了最大值,GetMin反之,注意Splay中还需要把找到的最值再Splay一下,否则无法保证均摊的复杂度。Prev是对当前值向左走一步然后一直向右,Next相反;对于Splay只要把它先伸展到根部再这样找就行了,但对于Treap,若它的左子树或右子树是空的,注意需要返回的反而是某个祖先节点,用递归实现或Buttom-Up比较简便。Rank操作只须记录每个节点的子树的大小,然后比较当前值与左子树的大小把它分到左边或者右边,如果分到右边要把左子树和当前节点的大小都加到答案里,一般就都采用非递归的实现了(反正是一个容易消除的尾递归)。Select基本与Rank类似,也是不断比较这个序号与左子树的大小,就能知道答案在左边还是右边了。

以上操作中GetMin、GetMax可以以每次操作相同的时间复杂度上界代替二叉堆,比单纯的二叉堆的优点是支持了方便的查找和删除,还可以同时找最大值和最小值。Prev、Next、Rank、Select等操作在很多题中也各有应用。

总的来说,对于这种用处广泛考察很多的数据结构来说,最好还是透彻地理解、熟练的背诵。

你可能感兴趣的:(ACM算法)