在堆排序算法中,使用的是最大堆,最小堆通常用于构造优先队列。
堆的数据结构
如果我们使用指针来表示堆有序的二叉树,那么每个元素都需要3个指针来找到它的上下结点(父结点和两个子节点各需要一个)。但如果使用完全二叉树来表示二叉堆,则只需要数组而不需要指针皆可以表示。具体方法就是将二叉树的结点按照层级顺序
放入数组中,根结点在位置1
[不使用数组的第一个位置0]。则结点k的父结点为k/2,两个子结点位置为k*2、k*2+1。
初级实现
- 数组实现(无序)insert和栈的push()操作一致;del需要添加一段类似于选择排序的内循环找到最大元素并删除
- 数组实现(有序)insert需要将所有较大元素向右移动以使数组保持有序;del和栈的pop()操作一致
上述两种初级方案中,插入元素和删除最大元素这两个操作之一在最坏情况下需要线性时间来完成。
使用无序序列是解决问题的惰性方法,仅在必要的时候才会采取行动(找到最大元素);使用有序序列则是解决问题的积极方法,因为会尽可能未雨绸缪(在插入元素时就保持列表有序),使后序操作更高效。
堆的有序化
上浮:如果堆的有序状态因为某个结点变得比它的父结点更大而打破,那么就需要通过交换它和它的父结点来修复堆。
void swim(int nums[], int k, int n) {
while (k > 1 && nums[k / 2] < nums[k]) {
swap(nums[k / 2], nums[k]);
k = k / 2;
}
}
下沉:如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小了而被打破了,那么就需要通过将它和它的两个子结点中的较大者
交换来修复堆。
void sink(int nums[], int k, int n) {
while (k * 2 <= n) {
int j = k * 2;
if (j < n && nums[j] < nums[j + 1]) j++;
if (!(nums[k] < nums[j])) break;
swap(nums[k], nums[j]);
k = j;
}
}
构建堆
堆排序之前首先需要将N个元素的数组构造一个堆。当然可以在O(NlogN)时间内,从左至右遍历数组,用swim()保证左侧的元素已经堆有序。更高效的办法是从右至左用sink()方法构造子堆。因为当用数组表示存储n个元素的堆时,叶结点下标分别是⌊n/2⌋ + 1, ⌊n/2⌋ + 2, ... , n。
每个叶结点都可以看成是只包含一个元素的堆。故这部分已经堆有序,我们只需要从n/2开始往前遍历一半元素,调用sink(),直到下标1即可。时间复杂度为O(NlogN)。
for (int k = n / 2; k >= 1; k--) {
sink(nums, k, n);
}
证明:因为当用数组表示存储n个元素的堆时,叶结点下标分别是⌊n/2⌋ + 1, ⌊n/2⌋ + 2, ... , n。
考虑⌊n/2⌋+1元素的左儿子:
LEFT(⌊n/2⌋ + 1) = 2(⌊n/2⌋ + 1)
> 2(n/2 − 1) + 2
= n − 2 + 2
= n.
由于左儿子下标已经超过了堆中元素个数n,故⌊n/2⌋ + 1结点无孩子,其为叶结点。对于下标更大的结点也是叶结点。
所以下标为⌊n/2⌋的结点不是叶子结点。当n为偶数,⌊n/2⌋结点左儿子下标为n,当n为奇数,有左儿子n-1,右儿子n。
堆排序
构建完最大堆之后,将堆中的最大元素删除,然后放入堆缩小后数组中空出的位置。这个过程和选择排序类似(按照降序取出所有元素),但所需的比较要少得多,因为堆提供了一种从未排序部分找到最大元素的有效方法
。
void heap_sort(int nums[], int n) {
for (int k = n / 2; k >= 1; k--) {
sink(nums, k, n);
}
while (n > 1) {
swap(nums[1], nums[n--]);
sink(nums, 1, n);
}
}
堆排序不是稳定的;是原地排序;时间复杂度为O(NlogN),空间复杂度为O(1)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。