分类目录:《算法设计与分析》总目录
相关文章:
·堆(一):基础知识
·堆(二):维护堆的性质
·堆(三):建堆
·堆(四):优先队列
·排序算法:堆排序
·利用heapq模块实现堆
我们可以用自底向上的方法利用过程heapify(arr, i)
把一个大小为
n
=
A
.
l
e
n
g
t
h
n=A. length
n=A.length的数组
A
A
A转换为最大堆。子数组
A
(
⌊
n
2
⌋
+
1
⋯
n
)
A(\lfloor\frac{n}{2}\rfloor+1\cdots n)
A(⌊2n⌋+1⋯n)中的元素都是树的叶结点。每个叶结点都可以看成只包含一个元素的堆。过程max_heap(A)
对树中的其他结点都调用一次heapify(arr, i)
def max_heap(A):
for i in range(int(len(A)/2) + 1, 0):
heapify(A, i)
为了证明max_heap(A)
的正确性,我们使用如下的循环不变量:在第2-3行中每一次for
循环的开始,结点计
i
+
1
,
i
+
2
,
⋯
,
n
i+1, i+2, \cdots, n
i+1,i+2,⋯,n都是一个最大堆的根结点。
我们需要证明这一不变量在第一次循环前为真,并且每次循环迭代都维持不变。当循环结束时,这一不变量可以用于证明正确性
- 初始化:在第一次循环迭代之前, i = ⌊ n 2 ⌋ i=\lfloor\frac{n}{2}\rfloor i=⌊2n⌋,而 n 2 + 1 , n 2 + 2 , ⋯ , n \frac{n}{2} + 1, \frac{n}{2} + 2, \cdots, n 2n+1,2n+2,⋯,n都是叶结点,因而是平凡最大堆的根结点。
- 保持:为了看到每次迭代都维护这个循环不变量,注意到结点
i
i
i的孩子结点的下标均比
i
i
i大。所以根据循环不变量,它们都是最大堆的根。这也是调用
heapify(A, i)
使结点i成为一个最大堆的根的先决条件。而且,heapify(A, i)
维护了结点 n 2 + 1 , n 2 + 2 , ⋯ , n \frac{n}{2} + 1, \frac{n}{2} + 2, \cdots, n 2n+1,2n+2,⋯,n都是一个最大堆的根结点的性质。在for
循环中递减i的值,为下一次循环重新建立循环不变量 - 终止:过程终止时, i = 0 i=0 i=0。根据循环不变量,每个结点 1 , 2 , ⋯ , n 1, 2, \cdots, n 1,2,⋯,n都是一个最大堆的根。特别需要指出的是,结点1就是最大的那个堆的根结点。
我们可以用下面的方法简单地估算max_heap(A)
运行时间的上界。每次调用heapify(A, i)
的时间复杂度是
O
(
log
n
)
O(\log n)
O(logn),max_heap(A)
需要
O
(
n
)
O(n)
O(n)次这样的调用。因此总的时间复杂度是
O
(
n
log
n
)
O(n\log n)
O(nlogn)。当然,这个上界虽然正确,但不是渐近紧确的我们还可以进一步得到一个更紧确的界。可以观察到,不同结点运行max_heap(A)
的时间与该结点的树高相关,而且大部分结点的高度都很小。因此,利用如下性质可以得到一个更紧确的界:包含
n
n
n个元素的堆的高度为
⌊
lg
n
⌋
\lfloor \lg n\rfloor
⌊lgn⌋。所以我们可以得到max_heap(A)
的时间复杂度为
O
(
n
)
O(n)
O(n)。