你的位置:首页 > 操作系统

[操作系统]算法—二叉查找树的相关一些操作及总结


二叉查找树得以广泛应用的一个重要原因就是它能够保持键的有序性,因此它可以作为实现有序符号表API中的众多方法的基础。这使得符号表的用例不仅能够通过键还能通过键的相对顺序来访问键值对。下面,我们要研究有序符号表API中各个方法的实现。

 1.最大键和最小键

如果根结点的左链接为空,那么一棵二叉查找树中最小的键就是根结点;如果左链接非空,那么树中的最小键就是左子树中的最小键。简单的循环也能等价实现这段描述,但为了保持一致性我们使用了递归。找出最大键的方法也是类似的,只是变为查找右子树而已。

2.向上取整和向下取整

如果给定的键key小于二叉查找树的根结点的键,那么小于等于key的最大键(floor)一定在根结点的左子树中;如果给定的键key大于二叉查找树的根结点,那么只有当根结点右子树中存在小于等于key的结点时,小于等于key的最大键才会出现在右子树中,否则根结点就是小于等于key的最大键。这段描述说明了floor()方法的递归实现,同时也递推地证明了它能够计算出预期的结果。将“左”变为“右”(同时将小于变为大于)就能够得到ceiling()的算法。向上取整函数的计算如下图所示。

3.选择操作

二叉查找树中的选择操作和之前我们学习过的基于切分的数组选择操作类似。我们在二叉查找树的每个结点中维护的子树结点计数器变量N就是用来支持此操作的。

4.select()方法

5.删除最大键和删除最小键

二叉查找树中最难实现的方法就是delete()方法,即从符号表中删除一个键值对。作为热身运动,我们先考虑deleteMin()方法(删除最小键所对应的键值对),如下图所示,和put()一样,我们的递归方法接受一个指向结点的链接,并返回一个指向结点的链接。这样我们就能够方便地改变树的结构,将返回的链接赋给作为参数的链接。对于deleteMin(),我们要不断深入根结点的左子树中直至遇见一个空链接,然后将指向该结点的链接指向该结点的右子树(只需要在递归调用中返回它的右链接即可)。此时已经没有任何链接指向要被删除的结点,因此它会被垃圾收集器清理掉。我们给出的标准递归代码在删除结点后会正确地设置它的父结点的链接并更新它到根结点的路径上的所有结点的计数器的值。deleteMax()方法的实现和deleteMin()完全类似。

6.删除操作

我们可以用类似的方式删除任意只有一个子结点(或者没有子结点)的结点,但应该怎样删除一个拥有两个子结点的结点呢?删除之后我们要处理两棵子树,但被删除结点的父结点只有一条空出来的链接。第一个方法,在删除结点x后用它的后继结点填补它的位置。因为x有一个右子结点,因此它的后继结点就是其右子树中的最小结点。这样的替换仍然能够保证树的有序性,因为x.key和它的后继结点的键之间不存在其他的键。我们能够用4个简单的步骤完成将x替换为它的后继结点的任务(具体过程如下图所示):

Ο将指向即将被删除的结点的链接保存为t;

Ο将x指向它的后继结点min(t.right);

Ο将x的右链接(原本指向一棵所有结点都大于x.key的二叉查找树)指向deleteMin(t.right),也就是在删除后所有结点仍然都大于x.key的子二叉查找树。

Ο将x的左链接(本为空)设为t.left(其下所有的键都小于被删除的结点和它的后继结点)。

7.范围查找

为了确保以给定结点为根的子树中所有在指定范围之内的键加入队列,我们会(递归地)查找根结点的左子树,然后查找根结点,然后(递归地)查找根结点的右子树。

8.性能分析

二叉查找树中和有序性相关的操作的效率如何?要研究这个问题,我们首先要知道树的高度(即树中任意结点的最大深度)。给定一棵树,树的高度决定了所有操作在最坏情况下的性能(范围查找除外,因为它的额外成本和返回的键的数量成正比)。

命题:在一棵二叉查找树中,所有操作在最坏情况下所需的时间都和树的高度成正比。

证明:树的所有操作都沿着树的一条或两条路径行进。根据定义,路径的长度不可能大于树的高度。

9.总结

在某些场景中二叉查找树在最坏情况下的恶劣性能仍然是不可接受的。二叉查找树的基本实现的良好性能依赖于其中的键的分布足够随机以消除长路径。对于快速排序,我们可以先将数组打乱;而对于符号表的API,我们无能为力,因为符号表的用例控制着各种操作的先后顺序。