二进制堆(binary heap)
一个二进制堆是一个完全二叉树结构,它要么是最小堆,要么是最大堆。在最大堆中,根部的键必须是堆中所有键的最大值,对于二叉树中的所有节点,同样的属性必须是递归的。最小堆与最大堆类似。 比如这是一个最大堆:
|
|
用数组表示堆
由于二进制堆是一个完全二叉树,所以它可以很容易地被表示为一个数组,使用数组表示可以节省空间。如果父节点存储在索引i
处,那么左子节点就在2 * i + 1
处,右子节点在2 * i + 2
处,比如:
|
|
如何构造堆
heapify
操作
我们以一个最大堆为例看看如何构造一个堆,这里是指从一个完全二叉树进行构造。
简单起见,我们先从一个只有三个节点的完全二叉树开始。
|
|
如上,如果根结点元素已经是最大的,我们不需要任何操作;
|
|
如果存在比根结点大的子节点,就需要交换以保持最大堆属性,也就是把顶层元素下降到合适的位置,这个操作称为heapify
。
再来看另一种情况:
|
|
顶层元素不满足最大堆的约束,但所有的子树都是最大堆,和前边三个元素的树的思路一样,我们需要把顶层元素2
下降到合适的位置:
- 找到
2
的子节点中比较大的那个,即10
,交换之
|
|
- 继续上一步操作,这次是
6
|
|
2
已经在叶节点,结束
我们可以看到:在两个子树都是最大堆的树中保持最大堆属性,我们需要在根元素上反复运行heapify
,直到它比它的子元素大,或者它成为叶节点,写成代码就是:
|
|
构建堆
有了heapify
这个函数之后我们就可以着手从一个完全二叉树构建出一个堆了,其思路就是从底层开始对每个子树进行heapify
操作,在heapify
函数应用于包括根节点在内的所有节点之后,我们就得到了一个最大堆。
对于一棵完全二叉树,最后一个非叶子节点的索引是n / 2 - 1
,之后的节点都是叶子节点,它们不需要heapify
操作,所以只需要从n / 2 - 1
向前一直遍历到根元素就可以了:
|
|
堆排序
堆排序有点类似于选择排序,只不过选择的过程利用了堆结构。
以从小到大的排序为例,我们可以用下述过程:
- 创建最大堆,此时最大的元素在根结点上,相当于我们利用一个最大堆选择出了最大值
- 把根结点上的最大元素和堆尾元素互换
- 将堆的尺寸缩小1,后边的元素已经排好序了,操作前边的元素就可以
- 由于把堆尾元素放到了根结点上,不能保证它是最大的,而左右子树都是最大堆
此时利用heapify
调整根结点,再次将二叉树变成最大堆 - 重复步骤2到步骤4,直到列表中的所有项目都被排序
|
|
堆排序在所有情况下(最佳情况、平均情况和最差情况)都有 O(nlog n) 的时间复杂度。这里只分析一下最差情况,其他情况分析思路相差不多。
在最差情况下,每一次Heapify操作都会将一个元素从当前位置移动到叶节点,需要进行log(n)次的比较和交换:
- BuildHeap阶段,需要为n/2个元素做Heapify操作,所以BuildHeap步骤的最坏情况的时间复杂度是n/2*log(n) ~ nlog(n)
- 排序阶段,我们将根元素与最后一个元素交换,然后对根元素进行Heapify操作。因为我们重复了n次,所以排序步骤的时间复杂度也是nlog(n)