前言
之前写过堆的实现,但是不够简单,应试考试中最好不要使用,会显得复杂。这次复盘是因为这是考研复试遇到的题目,发现自己对其掌握还是不够熟练,于是结合自己的复习过程对其进行总结和归纳。参考资料:王道数据结构视频+极客时间算法训练营。
一、什么是堆?
首先,堆是一组关键字序列,可以用数组进行存储,且下标从1开始,即Heap[1...n]。这时候我们需要回忆一下完全二叉树——除了最后一层外,其他任何一层的节点数均达到最大值,且最后一层也只是在最右侧缺少节点,我们会使用数组存储完全二叉树,且下标从1开始,那么下标为i的parent结点的左孩子的下标就是2i,右孩子的下标就是2i+1。堆的形态可以看做是具备某种特点的完全二叉树:
- 若Heap(i) < Heap(2i) 且 Heap(i) < Heap(2i + 1),我们称之为
小顶堆。也就是说小顶堆的每一个parent结点关键字比其child结点关键字小。 - 若Heap(i) > Heap(2i) 且 Heap(i) > Heap(2i + 1),我们称之为
大顶堆。大顶堆的每一个parent结点关键字比其child结点关键字大。 - i的范围是1 <= i <= (n/2向下取整)
下面可以对比其物理存储与逻辑存储的图解
二、怎样将无序序列转化成堆?
这里以大根堆为例。做法如下:
对所有parent结点(下标n/2向下取整~1)进行调整:
(1)出口:若孩子结点的关键字均小于双亲结点的关键字,则该双亲结点的调整完成。
(2)过程:若存在孩子结点的关键字大于双亲结点的关键字,则将最大的孩子结点与双亲结点进行互换(swap),指针移动到孩子位置并继续判断(1)与(2),直到出现(1)的情况,或者到达叶子结点。
过程图如下:
无序序列如图
调整过程如图:
(1)对下标最后的双亲结点进行调整,我们发现符合条件,不必调整
(2)指针前移,来到前一个双亲结点。
我们发现孩子结点17大于双亲结点13,所以把它们交换。
我们发现孩子结点是叶子结点,所以结束调整。
(3)此时指针来到下标为3的结点6.该结点的孩子结点都比6大,但是16更大,所以我们把6与16进行调换。
此时,指针来到橘色的孩子结点6,发现是叶子结点,结束调整。
(4)指针前移,来到下标为2的结点9.我们发现9<17<21,所以我们将9与21进行调换。
指针来到调换后的结点9,我们发现9<12,于是又开始调换。
调换结束后,指针来到叶子结点,结束调整。
(5)指针前移,来到下标为1的结点14,采用上述方法进行调整即可。
检查完成,此时大顶堆构建完毕。
代码如下:
#include<stdio.h>
void AdjustDown (int A[], int k, int len) {
A[0] = A[k]; //哨兵保存双亲结点关键字
int i;
for (i = 2 * k; i <= len; i *= 2) {
if (i < len && A[i] < A[i+1]) //右孩子>左孩子,指针移到右孩子
i++;
if (A[0] >= A[i]) //哨兵关键字比孩子结点关键字最大值大,出口
break;
else {
A[k] = A[i];// 孩子结点的值赋给双亲结点
k = i;// 指针指向孩子结点
}
A[k] = A[0]; //将哨兵的关键字保存到调整结束的位置
}
}
void BuildMaxHeap( int A[], int len ) {
int i;
for (i = len / 2; i > 0; i--)
AdjustDown(A, i, len);
}
int main(int argc, char const *argv[]) {
int A[11] = {0, 14, 9, 6, 13, 21, 10, 16, 17, 2, 12};
BuildMaxHeap(A, 10);
int i;
for (i = 1; i <= 10; i++)
printf("%4d",A[i]);
return 0;
}
测试结果:
三、堆排序的实现原理
我们已经了解了建立堆的过程,我们发现最后一次调整是对顶点进行调整,而堆的顶点恰恰是当前堆的最大值。提醒到这里,相信你一定发现了,进行堆排序时(这里大顶堆就是形成从大到小的有序序列),我们将大顶堆的堆顶元素输出,然后将其与最后一个元素进行互换,并将堆的长度减小1,此时再对当前堆进行调整,重新形成大顶堆。以此类推,直到最后只剩下一个元素,输出后,堆长度为0,排序结束。
过程图如下:
输出序列:21
然后准备与最后一个结点交换
此时堆长度-1,即灰色的结点已经被我们在逻辑上剔除。此时的新堆顶关键字是9,我们需要对它进行调整,让新堆变成大顶堆。
调整后,我们得到新堆如下所示。我们输出堆顶结点关键字17,然后准备与当前新堆的绿色结点(当前新堆的最后一个元素)进行调换。
输出序列:21 17
调整过后,我们又得到了新堆,但依然需要对堆顶结点进行调整。并且删掉刚刚输出的结点(灰色结点)
调整后新的大顶堆如图所示
以下的过程可以依次类推。
这里代码实现略有出入,因为我们在调整之后才对数组进行输出,所以我们最终会得到一个从小到大排序的有序序列。下面的代码需要结合上述代码中的函数。
void swap (int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void HeapSort ( int A[], int len) {
int i;
BuildMaxHeap(A, len);
for (i = len; i > 1; i--) {
swap(&A[i], &A[1]);
AdjustDown(A, 1, i-1);
}
}
int main(int argc, char const *argv[]) {
int A[11] = {0, 14, 9, 6, 13, 21, 10, 16, 17, 2, 12};
HeapSort(A, 10);
int i;
for (i = 1; i <= 10; i++)
printf("%4d",A[i]);
return 0;
}
测试结果:
四、递归写法实现
void heapify(int array[], int length, int i) {
int left = 2 * i + 1, right = 2 * i + 2;
int largest = i;
if (left < length && array[left] > array[largest]) {
largest = left;
}
if (right < length && array[right] > array[largest]) {
largest = right;
}
if (largest != i) {
int temp = array[i]; array[i] = array[largest]; array[largest] = temp;
heapify(array, length, largest);
}
}
void heapSort(int array[], int length) {
int i;
if (length == 0) return;
for (i = length/2 - 1; i >= 0; i--)
heapify(array, length, i);
for (i = length - 1; i >= 0; i--) {
int temp = array[0]; array[0] = array[i]; array[i] = temp;
heapify(array, i, 0);
}
}
五、总结
堆排序的时间复杂度是O(nlogn),空间复杂度为O(1),是不稳定的选择排序算法。
本文使用 mdnice 排版