heap中的heapify与依次压入队列的差异

标准的堆插入元素的算法很好理解,而且也很容易知道向堆中插入一个元素的代价是lgn。

按照最常规的想法,把一个数组中所有元素添加到一个堆中,依次压入即可。压入n个元素的代价就是

Sum[Log2[i],{1,i,n}]。

结果等于Log2[n!].根据斯特林公式,n!在n趋向于无穷大时可以近似看成(2Pi*n)^1/2*(n/E)^n。

因此,总代价可以看成n*Log2[n/E]+o(n),比O(n)的阶要大一些。

直觉上,这个操作的复杂度应该是要比这个低的。因为将n个元素完整排序的代价不过是nlgn。而堆应该是一个半排序。不应该接近nlgn。

这个直觉是正确的,heapify操作正是一个线性时间的算法.

举个python的标准库heapq里面的实现

def cmp_lt(x, y):
    # Use __lt__ if available; otherwise, try __le__.
    # In Py3.x, only __lt__ will be called.
    return (x < y) if hasattr(x, '__lt__') else (not y <= x)
def heapify(x):
    """Transform list into a heap, in-place, in O(len(x)) time."""
    n = len(x)
    # Transform bottom-up.  The largest index there's any point to looking at
    # is the largest with a child index in-range, so must have 2*i + 1 < n,
    # or i < (n-1)/2.  If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so
    # j-1 is the largest, which is n//2 - 1.  If n is odd = 2*j+1, this is
    # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.
    for i in reversed(xrange(n//2)):
        _siftup(x, i)
def _siftdown(heap, startpos, pos):
    newitem = heap[pos]
    # Follow the path to the root, moving parents down until finding a place
    # newitem fits.
    while pos > startpos:
        parentpos = (pos - 1) >> 1
        parent = heap[parentpos]
        if cmp_lt(newitem, parent):
            heap[pos] = parent
            pos = parentpos
            continue
        break
    heap[pos] = newitem
def _siftup(heap, pos):
    endpos = len(heap)
    startpos = pos
    newitem = heap[pos]
    # Bubble up the smaller child until hitting a leaf.
    childpos = 2*pos + 1    # leftmost child position
    while childpos < endpos:
        # Set childpos to index of smaller child.
        rightpos = childpos + 1
        if rightpos < endpos and not cmp_lt(heap[childpos], heap[rightpos]):
            childpos = rightpos
        # Move the smaller child up.
        heap[pos] = heap[childpos]
        pos = childpos
        childpos = 2*pos + 1
    # The leaf at pos is empty now.  Put newitem there, and bubble it up
    # to its final resting place (by sifting its parents down).
    heap[pos] = newitem
    _siftdown(heap, startpos, pos)


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]。单纯计算比较次数

在最坏情况下,逐个插入算法

1.插入a

2.插入b,a与b比较一次

3.插入c,a鱼c比较一次

4.插入d,d与b比较一次,再与a比较一次

5.插入e,e与b比较一次,再与a比较一次

6.插入f,f与c比较一次,在与a比较一次

7.插入g,g与c比较一次,再与a比较一次


共计比较10次。


而heapify算法:

1.a与b、c各比较一次

2.d与e、f各比较一次

3.a与d比较一次

4.g与a比较一次

5.b与c比较一次

6.g与b比较一次


共计比较8次。


你可能感兴趣的:(heapify)