赞
踩
接下来这一节,来讨论完全二叉堆的元素删除算法。
我们知道,对于优先级队列而言,内容读取操作只限定于特定的节点。 具体来说只能是获取最大元或者删除最大元。
我们也知道,得益于堆序性,完全二叉堆中的最大元必然就是堆顶。因此,如果只是单纯地获取这个元素,我们只需常数的时间。然而为了删除这个节点,我们却需要做更多额外的工作。
首先,我们自然是将这个节点在物理上摘除掉,当然此后在逻辑上这就不再是一个结构完整的完全二叉树。为尽快地恢复结构性,不防将末元素移送到首元素的位置上,以取代这个被摘除的堆顶。经如此处理之后,结构性的确得以恢复,然而堆序性却有可能因此遭受破坏。
我们来观察一下,再将末元素前移至堆顶之后,在哪些位置有可能会违背堆序性呢?从图中不难看出,唯一的可能只能是这个新的堆顶节点与它的孩子们违反堆序性,那么,如何加以修复呢?
没错,交换。 如果 e 的确小于它的至少一个孩子,我们就将它与其中更大的那个孩子互换位置,交换之后的情况应该是这样(上图上左3)。可以看到,在经过这样一次交换之后,在最高的层次上已经不再会出现逆序的情况。 当然,其它的节点也依然会维持原有的堆序性。
然而故事可能依然没有完结,因为下降一层之后的 e 又会有新的孩子,而且如果不幸,它有可能至少会小于两个孩子中的某一个。看得出来如何解决这个新的难题吗?没错,依然是交换。在这种情况下,我们需要将 e 与它更大的那个孩子进行交换。 就这个例子而言,交换之后的结果应该是这样(上图下左1)。当然,经过这样的一次交换之后,在这个层次以上的所有逆序缺陷都应该能够得以修复。
然而不幸的是,在 e下降一层之后,在接下来的这个层次上有可能会再次发生逆序。不过我们对此已经习以为常了。果真出现这种情况的话,我们也可以故伎重演,令e和它更大的那个孩子互换位置。就这个例子而言,交换之后的效果是这样(上图下左2)。当然这种情况的确可能会再次的以及持续地发生下去,不过不要紧,每次出现这样的情况,我们都可以通过交换来加以解决。
与节点的插入过程相仿,这里也存在某种单调性。在原本最底层的节点 e 被前置为堆顶之后,每经过一次交换,它都会下降一层。这就意味着尽管逆序的情况有可能多次的持续发生,但问题出现的高度必然会持续地下降。因此这样一个过程也形象地称作为"下滤"( percolate down),正是得益于这种不断下降的单调性,我们才可以保证这个算法必然终止。
来看这样一个具体的实例。
因此最终整个数据结构又重新恢复为一个完全二叉堆。
那么,这样一个连续不断地逐层下滤的过程又当如何具体实现为代码呢?实现的效率又能达到多高?
这里,我们就给出完全二叉堆节点删除算法的一种可能实现。
总而言之,一旦while循环退出,我们即可返回当前的下滤终点,整个算法也随即大功告成。
这个算法的正确性不难理解,而接下来更为重要的一个问题自然又是这个算法,或者更准确地讲,这个主体的循环需要耗费多少时间呢?其总体的效率是否和插入算法一样,也能达到我们预期的设计目标呢?
纵观整个下滤调整的过程,我们所做的工作主体无非两类:第一类也就是所谓的“比较”,第二类则是交换,非常幸运的是,我们每下滤一层,这两类操作都只需要执行常数次,因此就渐进意义而言,整体的时间复杂度不会超过堆的高度。
与上滤算法一样,作为完全二叉树,这里堆的高度也绝不会超过 log(n),这也是整个删除算法的渐进时间复杂度。
当然就常系数意义而言,我们依然存在改进的余地,比如交换操作所涉及的3 * log n 次赋值语句同样可以优化为 log n。
最后一点需要指出的是,下滤过程中的比较操作,与上滤过程中的比较操作在模式上和成本上都有实质上的区别。你应该记得,在上滤过程中,每个节点只需和它唯一的那个父亲进行比较。也就说,在每一层次上我们只需要一次比较。而下滤过程的每一步所涉及的都是一个节点以及它的两个孩子。为了从它们当中找出最大的那个,我们不得不做两次比较,当然对于二叉堆来说还不是什么了不起的差异,而在多叉堆中,这一差异将会变得至关重要。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。