一 堆排序的定义
堆的定义,n个关键字序列L[1...n]称为堆,当且仅当序列满足:
1. L[i]≥L[2i] 且 L[i]≥L[2i+1](大根堆)
2. L[i]≤L[2i]且L[i]≤L[2i+1] (小根堆)
i需要满足的特点为(1≤i≤⌊n/2⌋)
可以将一维数组视为一棵完全二叉树,满足条件1的堆称为大根堆,大根堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点值。满足条件2的堆称为小根堆,小根堆的根节点是最小值,且其任一非根结点的值大于等于其双亲结点值。
大根堆:
对于所有具有双亲结点含义编号从大到小(⌊n/2⌋~1)做出如下调整:
- 若孩子结点皆小于双亲结点,则该结点的调整结束。
- 若存在孩子结点大于双亲结点,则将最大的孩子结点与双亲结点交换,并该孩子结点进行同样的交换,知道孩子结点为叶子结点为止。
二 堆排序思路
堆排序首先将存放在L[1...n]中的n个元素建成初始堆,由于堆本身的特点(以大根堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大根堆的性质,堆被破坏,将堆顶元素向下调整令其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。
堆排序需要解决的问题:
- 如何将无序序列构造成初始堆。
- 输出堆顶元素后,如何将剩下元素调整成新的堆。
堆排序的关键是构造初始堆,n个结点的完全二叉树,最后的一个结点是第n/2个结点的孩子结点。对第n/2个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大值,则交换),使得该子树成为堆。之后向前依次对各结点([n/2] - 1 ~ 1)为根的子树进行筛选,看该结点是否大于其左右子节点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆。直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。
如图:初始时调整L[4]子树,将09与32交换,交换后满足堆的定义;向前继续调整L[3]子树,78小于左右孩子的较大者87,交换,交换后满足堆的定义;向前继续调整L[2]子树,17小于左右孩子的较大者45,交换后满足堆的定义;向前调整至根结点L[1],53 < 左右孩子的较大者87,交换,交换后破坏了L[3]子树的堆,采用上述方法堆L[3]进行调整,53 < 左右孩子的较大者78,交换,至此该完全二叉树满足堆的定义。
输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将09和左右孩子的较大者78进行交换,交换后破坏了L[3]子树的堆,继续对L[3]子树向下筛选,将09和左右孩子较大者65交换,交换后得到新堆。
三 堆排序的算法代码
#include <stdio.h>
#include <stdlib.h>
void swap(int* a, int* b)
{
int temp = *b;
*b = *a;
*a = temp;
}
void max_heapify(int arr[], int start, int end)
{
//建立父节点指标和子节点指标
int dad = start;
int son = dad * 2 + 1;
while (son <= end) //若子节点指标在范围内才做比较
{
if (son + 1 <= end && arr[son] < arr[son + 1])
//先比较两个子节点大小,选择最大的
son++;
if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
return;
else{ //否则交换父子内容再继续子节点和孙节点比较
swap(&arr[dad], &arr[son]);
dad = son;
son = dad * 2 + 1;
}
}
}
void heap_sort(int arr[], int len)
{
int i;
//初始化,i从最后一个父节点开始调整
for (i = len / 2 - 1; i >= 0; i--)
max_heapify(arr, i, len - 1);
//先将第一个元素和已排好元素的前一位做交换,再重新调整,直到排序完毕
for (i = len - 1; i > 0; i--)
{
swap(&arr[0], &arr[i]);
max_heapify(arr, 0, i - 1);
}
}
int main() {
int arr[] = {49,38,65,97,76,13,27,49};
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
代码结果:
四 堆排序性能分析
算法 | 最好时间 | 最坏时间 | 平均时间 | 额外空间/空间复杂度 | 稳定性 |
---|---|---|---|---|---|
堆排序 | O(nlog2^n) | O(nlog2^n) | O(nlog2^n) | O(1) | 不稳定 |
空间效率:仅使用常数个辅助单元,所以空间时间复杂度为O(1)。
时间效率:建堆的时间为O(n),之后有n-1此向下调整操作,每次调整的时间复杂度为O(log2^n),故在最好,最坏和平均情况下,堆排序的时间复杂度为O(nlog2^n)。
稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。表L={1,2,2}
构造初始堆时可能将2交换到堆顶,此时L={2,1,2},最终排序序列为L={1,2,2},由此可知,堆排序是一种不稳定的排序。