赞
踩
好,接下来我们就来学习在一个完全二叉堆中,如何有效地插入一个新的元素。我们将会看到插入过程中的核心技巧是所谓的 ”上滤“ 过程。
为了在完全二叉堆中引入一个新的词条 e,我们只需在物理上将它作为末元素,直接插入至对应的向量之中。没错,向量。
请记住,尽管在逻辑上我们可以将优先级队列理解为一棵完全二叉树,但在物理上它依旧是一个不折不扣的向量,向量在物理上增加一个末元素,等效于在这棵完全二叉树底层向空缺的部分拓展一个节点。
可以看到,将新的词条作为末元素接入向量的好处在于,可以使得完全二叉堆的结构性得以延续。当然,另一条件,也就是堆序性如果也能够得以延续,那么我们也就大功告成。然而很遗憾,在新的节点引入之后,堆序性未必能够延续。
当然,即便如此,情况也不致太糟糕。具体来说,唯一可能违背堆序性的只有新插入的这个节点与它的父亲。也就是说,新插入的这个节点拥有一个父亲,而且在数值上,新插入的这个节点要大于它的父亲。
既然找到了问题的症结,我们也就自然得到了解决的办法。没错,在这种情况下我们只需将新插入的节点 e 与它的父亲互换位置,从而转换为这样一种状态。不难看出经过这样的转换之后,不仅顺利地解决了 节点e 与它此前的父亲之间的逆序性,同时其他所有节点之间的堆序性也不致受到影响。当然,问题未必就此完全地解决。
因为 e 有可能依然会有一个新的父亲。而且如果不巧,e 有可能依然会大于这个新的父亲,也就是说,e 和它新的父亲再次违反堆序性。你应该知道如何解决这个问题了。是的,我们只需再次套用此前的策略,令 e 和它新的父亲互换位置,从而进入这样一种状态。同样经过这样一次交换,在刚才这一层的逆序性得到了修复,而且同时不致影响到其他的节点。接下来如果依然存在问题,也只可能是 e 和它这个最新的父亲违背堆序性。果真如此,我们可以继续令e和它的父亲互换位置,从而转入这样一种状态。
好消息是,这样一个反复交换的过程,满足某种单调性。 不难看出,每经过这样一次交换,e 的高度就会上升一层,同时违反堆序性的情形如果存在,也必然会上升一层。这一过程,亦即所谓的上滤(percolate up)
既然这样一个逐层调整的过程充其量不过抵达到树根,因此也迟早会终止于某个位置。而一旦这个过程终止,也就意味着堆序性在整个完全二叉堆中得到了彻底的恢复。
注意:上图所标注的数值都是元素的优先级而非其所对应的秩。
我们来看一个简单的实例,不难验证,这是一个由5个词条所构成的完全二叉堆。上面的这棵完全二叉树是它的逻辑结构,而下面则是其在物理上对应的向量结构。
我们也就顺利地完成了这次插入操作。可以看到,整个插入算法的实质过程,无非是令新引入的这个节点不断地与它的父亲交换位置。每交换一次,新引入的这个节点都会上升一层。那么这样一个过程如何兑现为具体的代码呢?
在这里我们给出完全二叉堆插入算法的一种可行实现方式:
这个算法的正确性不难验证。那么接下来的问题便是这个算法需要运行多长时间呢?对应的时间复杂度是否能达到我们所预期的目标呢?
应该记得,在完全二叉堆中插入新元素之后的调整过程之所以称作是”上滤“,是因为每经过一步迭代,新插入元素的位置都会上升一次。换而言之,整个算法在完全二叉堆的每一层次上至多只需做一步迭代。
我们知道完全二叉树是理想平衡的二叉树,其树高可以严格地控制在 log( n) 的范围内。我们刚才也已看到,每一步迭代只需常数的时间。因此所有的迭代所需的时间累计也不过log( n) 。
就渐进的意义而言,这已经实现了我们最初的设计目标。当然就常系数的意义而言,这里依然还有改进的余地。你应该记得,在我们刚才所给出的实现中,每一次交换都是通过一个名为 swap 的过程来完成。这样的一次操作通常都意味着三次赋值操作,因此在最坏情况下,我们累计需要做多达 3*log( n) 赋值。
针对这一问题,一种简明的改进方法就是,首先将新插入的词条做备份,每次如有必要交换,我们只是下移它的父节点。直到能够确定 e 已经无需继续上滤时,我们才将此前备份的这个词条纳入于最终的这个位置。 如此我们就可将赋值操作的次数从 3*log( n) 减至 log( n) + 2,从而在常系数意义上有所改进。
当然,另一类操作,也就是父子词条之间的大小比较操作也可以有一定的改进。
关于完全二叉堆,另一个好消息是它的平均性能要远远优于刚才所分析的最坏情况。 实际上,新插入节点需要持续上升足够多层,乃至最终能够抵达树根的情况,出现的可能性是极低的。更加精细的估算表明,在通常的随机分布下,每个节点上升的平均高度实际上只不过是常数。这也是完全二叉堆低成本、高效率的重要证据。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。