标准的堆插入元素的算法很好理解,而且也很容易知道向堆中插入一个元素的代价是lgn。
按照最常规的想法,把一个数组中所有元素添加到一个堆中,依次压入即可。压入n个元素的代价就是
$\sum_{i=1}^N\log_2i$
结果等于$log_2n!$.根据斯特林公式,n!在n趋向于无穷大时可以近似看成$\sqrt{2\pi n} ({n\over e})^n$。
因此,总代价可以看成$n \log_2({n\over e})+o(n)$,比O(n)的阶要大一些。
直觉上,这个操作的复杂度应该是要比这个低的。因为将n个元素完整排序的代价不过是nlgn。而堆应该是一个半排序。不应该接近nlgn。
这个直觉是正确的,heapify操作正是一个线性时间的算法.
举个python的标准库heapq里面的实现
|
|
heapify操作主要有3个过程,heapify对一半的元素调用_siftup,而_siftup又调用了_siftdown。
这个过程作用的原理是什么?
_siftup(heap,pos)最开始部分的代码很明显,就是把pos处的元素沿着最小路径一路向下降到底。途中所比较的子节点依次上浮一层。而_siftdown(heap,startpos,ps)过程则是标准的堆插入动作,将pos处的元素上浮直到小于startpos时停止,也就是startpos的左兄弟节点或上一层节点。
这可以理解为一个从倒数第二层开始构建堆的过程。第一轮循环把最下面两层的3个节点的元素变成堆,再依次向上,通过插入一个上一层元素,将两个堆合并。因为每上一层,这层下面的两个元素分别都是这两棵子树的最小元素。
这个过程可以看成类似数学归纳法的原理。第一个元素成立,而n成立确保n+1成立,因此对所有元素就成立。逐个插入的问题规模是线性增长的,比如比n小的元素是n-1,而归并操作的问题规模是折半的。这就相当于在逐次插入元素的时候,当插到第四个元素的时候,停止向原堆插入,而向新堆插入。当两个堆平衡以后,再将两个堆合并。
因此,他的时间T(n)=2T(n/2)+2lg(n/2)
这刚好落入了主方法的第一种情况,因此他的复杂度是O(n)
举个7个元素的例子。x=[a,b,c,d,e,f,g]。单纯计算比较次数
在最坏情况下,逐个插入算法
- 插入a
- 插入b,a与b比较一次
- 插入c,a鱼c比较一次
- 插入d,d与b比较一次,再与a比较一次
- 插入e,e与b比较一次,再与a比较一次
- 插入f,f与c比较一次,在与a比较一次
- 插入g,g与c比较一次,再与a比较一次
共计比较10次。
而heapify算法:
- a与b、c各比较一次
- d与e、f各比较一次
- a与d比较一次
- g与a比较一次
- b与c比较一次
- g与b比较一次
共计比较8次。