注:这里的堆还是以小根堆为例。
我们想要设计一种堆能像二叉堆那样高效地支持合并操作,也就是$O\left( n \right)$时间处理一次Merge,而且只使用一个数组看起来很困难对吧,毕竟合并操作需要把一个数组复制到另外一个数组中去,对于相同大小的堆这会花费$\theta \left( n \right)$。也正因如此,我们可以看到,在此前所有支持高校合并的高级数据结构都需要用到指针。但是!在实践中就有一些问题,它会使操作效率变低,因为处理指针一般而言比乘除法操作更耗费时间。
那该怎么做呢?二叉堆进化——左式堆,又叫左偏树。像二叉堆那样,左式堆也具有结构特性和有序性,有着和二叉堆相同的heap property。他和二叉堆的唯一区别是:左式堆不是理想平衡的,而是趋向于极其不平衡,在拓扑形态上更倾向于向左倾斜的。那么,为什么要引入这一新的变种呢?让我们从它的设计动机以及结构定义说起。前面说了,我们的目的是完成高效合并,现有的很多方法已经足够合并了,但是太慢。而数据结构的精髓就是不断优化性能,所以需要引入新的结构来完成这个目的。下面先来逐一分析各种拍脑袋的算法,然后逐渐逼近这次的主角。
最平凡的思路是以大的为基础,把小的堆里元素逐一取出插入到A中,B为空时就完成了。
可以一句话概括:Insert(DeleteMax(B),A);
但这太慢了,简直龟速。分析一下,我们把两个堆的规模记作n和m,不失一般性地,假设A不小于B,也就是:$\left| A \right|\; =\; n\; \; \geq \; m\; =\; \left| B \right|$。所以整个算法迭代$m$次,在每一次里deleteMax(B)要花费$\log m$的时间,把这个元素汇入A中花费$\log \left( n\; +\; m \right)$的时间。所以一共是$O\left( \; m\; \cdot \; \left( \log m\; +\; \log \left( n\; +\; m \right) \right) \right)\; =\; O\left( m\; \cdot \; \log \left( n\; +\; m \right) \right)\; =\; O\left( m\cdot \; \log n \right)$的时间。恐怕你自己恐怕都不满足这个效率,因为的确有改进的空间。我们会再想起Floyd批量建堆算法,嗯没错,更高效的办法是先把两个堆混合起来,然后通过下滤,维护整个堆的结构特性,把它整理为一个完全二叉堆。概括一下就是BuildHeap(n+m, union(A,B));而Floyd算法只需要线性时间,也就是总共$O\left( n\; +\; m \right)$,这个效率就高一些了。
但这还不能令人满意,原因在于:Floyd算法的输入默认是无序的,而我们的两堆分别都是有序的,刚才这个算法没有利用到我们已知的信息,如果利用上了这部分有序的信息,就可以加速执行了。从这个角度出发,我们有理由相信:一定存在更高效的数据结构和相应算法。的确如此,Clark Allan Crane历经探索,发明了一个新的结构:左式堆,并于1972年以此发表了他的博士论文$Linear\; Lists\; and\; Priority\; Queues\; as\; Balanced\; \mbox{Bi}nary\; T\mbox{re}es$。这种结构在保持堆序的前提下附加少量条件,就能在合并过程中只需要调整很少的节点,插入和删除都仅需要$O\left( \log n \right)$的时间。比刚才的$O\left( n\cdot \log n \right)$和$O\left( n \right)$都有了长足的进步。他的这个新条件就是“单侧倾斜”,节点分布都偏向左侧,而算法高效的诀窍是合并操作只涉及右侧,而右侧节点很少。
比如这就是左式堆的典型图解,左长右短,它可以把右侧节点严格控制在$O\left( \log n \right)$以内,这也印证了上面说的合并操作的复杂度在O(logn)范围内。这也引发出一个定理:在右侧路径上有 $r$ 个节点的左式堆必然至少有 $2^{r\; }-\; 1$ 个节点。