文章目录
- 1.完全二叉树
- 2.结构性
- 3.形神具备
- 4.堆序性
1.完全二叉树
在上一节我们看到,就优先级队列的实现方式而言,采用基本的向量结构并不足够,而采用更高级的树形结构,虽然完全可以高效率地实现优先级队列,但却有杀鸡用牛刀之嫌。那么能否在这两种策略之间设计一种折中的方案呢?
答案是肯定的,为此我们需要以向量为形,以树形结构为神,实现二者之间的有机结合。
为此,我们需要借助完全二叉树。所谓的 ”Complete Binary Tree“, 可以认为是 AVL 树的一个特例,其中的平衡因子处处非负。也就是说各节点的平衡因子或者为0,或者为 +1,但绝对不可能是-1。
而且对于树中的任何一个节点 V 而言,如果它的平衡因子为 0,那么左子树就必然是满树。另一种可能,如果 v 的平衡因子为 +1,那么它的右子树就必然是满树。由这些约定不难推知,就全局的拓扑结构而言,完全二叉树大致应该呈现为这样一种形式。
也就是说,如果忽略它的最底层,那么其余节点应该组成一棵满树。而最底层也只不过是缺失了右侧的连续一段。
2.结构性
比如,这就是一棵完全二叉树。可以看到,相对于满树,它的确只不过是在最底层的右侧缺失了连续的若干个节点。
现在回到优先级队列的实现问题,我们的思路是在逻辑上将优先级队列等同于一棵完全二叉树。而在物理上,我们却可以直接借助更为简明的向量来直接实现。 没错,通过向量来表示一棵完全二叉树,并且进而实现优先级队列。这一思路之所以可行,要得益于完全二叉树在结构上的紧凑性。
来看这样一个向量。没错,这就是一个向量,只不过为了便于与完全二叉树相对照,这里将它切分成了若干段。在这个例子中我们不难看出,完全二叉树中的每一个节点都与向量中的某一个节点相对应。
反过来,向量中的每一个元素也都与完全二叉树中的某一个节点相对应。更准确地讲,完全二叉树中的节点与向量中的元素之间是彼此一一互相对应的。是的,稍加观察就不难发现:在这里逻辑节点与物理元素之间的一一对应关系,与完全二叉树的层次遍历次序完全一致。
因此我们可以在向量内的各元素之间定义父与子的关系。具体来说,对于向量中任意秩为 i 的元素,它的父节点如果存在,其秩就必然等于(i - 1)/2。
比如对于秩为11和12的元素而言,它们的父节点的确存在,而且编号应该为5。
反过来,向量中秩为 i 的元素,如果拥有左孩子,那么左孩子所对应的秩就应该是 i * 2 + 1。
比如对于秩为6的这个元素而言,它的左孩子如果存在,它的秩应当是 6 * 2 +1 = 13,6的左孩子为13。
类似地,任何一个秩为 i 的元素,它的右孩子如果存在,那么它所对应的秩就应该是 (i + 1)* 2。
同样以这个秩为 6 的元素为例,其右孩子的秩应该是 (6 + 1)* 2 = 14,6的右孩子为14。
由此可见,我们的确可以在向量与完全二叉树之间建立这样一种对应的关系。请留意体会这种对应关系的精妙之处。实际上在物理上我们没有做任何的改动,所有的元素依然构成一个线性的向量。
但是,只要我们聪明地变化一下视角,站在这种对应关系的角度来重新审视这个向量,就会发现其实它的确是一个不折不扣的完全二叉树。这也为我们实现优先级队列提供了第一种可能。因为这种方法在逻辑上借助了完全二叉树,因此我们也将这种实现方法称作完全二叉堆。
那么具体的又当如何将向量的形与树”形“结构的“神”有机的融合起来呢?
3.形神具备
在这里,借助 C++语言中的多重继承特性,将向量模板类与优先级队列接口有机的融合起来,从而定义出完全二叉堆模板类。
这就是具体的实现方法,可以看到,这里的 PQ_ComplHeap 以公开的模式同时继承了 PriorityQueue 和 Vector 的特性。也就是说在物理上它自然就拥有了一个名为 elements 的可扩充数组。
另外一方面,作为 PriorityQueue 的派生类,它也自然应该提供我们此前所介绍的三种标准接口。此外还提供一个批量构造的接口,这些接口的实现方法我们都将在稍后逐一介绍。
当然,这些对外操作接口的实现还需要依赖若干内部的功能接口,我们也会在稍后相应地介绍它们。至此,我们已经在物理上的向量与逻辑的优先级队列之间初步地建立起了联系。然而,为了使得这种联系具有神采,我们还需要做另一方面的工作。
4.堆序性
如果说以上的结构性使得完全二叉堆拥有了血肉,那么堆序性就是它的灵魂。也就说,需要在完全二叉堆的所有节点之间定义某种次序。实际上,关于这一次序的约定并不复杂,简而言之,也就是任何一个节点在数值上都不会超过它的父亲。
请注意,在我们刚才所使用的这幅图中,每个节点上所标注的只是它所对应的元素在向量中的秩,而不是真正的数值。
而按照堆序性,任何节点都不得超过它的父亲。反过来,如果一个节点拥有左孩子和右孩子,那么在数值上,它也不会小于它的任何一个孩子。
回到优先级队列的功能接口,其中所涉及的最大元,在完全二叉堆中,又可能在哪里呢?根据堆序性不难推知,最大元只可能是根节点,难道不是吗?否则的话,如果最大元不再根节点处,那么就意味着它必然拥有父亲。而根据堆序性,它的父亲要比它更大。
刚才已经讲过,在物理上,根节点在向量中所对应的向量元素的秩应该必然是0。因此我马上就会发现,按照这种实现方法,getMax() 只需返回内部数组中的首元素。
好消息是,按照这样一种低成本的实现方式,其他的操作接口也同样能够高效率地加以实现。