一.堆排序基本概念
再了解堆排序之前,首先要明白堆是什么。
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;
或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
对于完全二叉树有些性质也要提前进行了解才能充分了解堆排序,这里就不再多说了。
堆排序就是利用堆(假设为大顶堆)进行排序的方法,其思想分为两个部分:
1.大顶堆的初始构建;
2.将堆顶元素移除后,大顶堆的重建。
二.具体实现
堆排序的主要内容就是大顶堆的构建,无论是初始构建还是重构都是这一过程。
1.大顶堆的构建
构建大顶堆的核心思想就是逐步比较双亲结点和孩子结点的值来使得所有双亲结点的值都大于孩子结点
这里通过一个例子来进行理解
构建大顶堆也称为大顶堆的调整,代码如下
//s为堆顶元素的下标
e为末尾元素的下标
void heapadjust(int* a, int s, int e)
{
int tem = a[s]; //tem用来保存堆顶元素
for (int j = 2 * s; j <= e; j *= 2) // 为什么j初始化为2*s、j为什么二倍的递增,这就要了解二叉树的性质,这里的j指的是s的左孩子。
{
if (a[j] < a[j + 1] && j < e) j++; //判断左右孩子谁更大
if (tem >= a[j]) break; //如果双亲大于孩子就无需进行调整,跳出循环
a[s] = a[j]; // 此时双亲小于孩子,使双亲得到孩子的值
s = j; // 更换双亲进入下轮循环比较
}
a[s] = tem; //确定孩子的值
}
2.堆排序的主体框架
因为堆排序主要是大顶堆初始构建和重构
所以排序主体代码也有两部分组成
void sort(int* a, int len)
{
int j;
for (j = (len-1) / 2; j > 0; j--) //大顶堆的初始构建
{
adjust(a, j, len - 1);
}
for (j = len - 1; j > 0; j--) //后续大顶堆重建
{
swap(a, 1, j);//交换堆顶元素和末尾
adjust(a, 1, j - 1);//对剩下的元素进行大顶堆重建
}
}
void swap(int* a, int b, int c)
{
int tem = a[b];
a[b] = a[c];
a[c] = tem;
}
三.具体代码
#include<stdio.h>
void sort(int* a, int len);
void swap(int* a, int b, int c);
void adjust(int* a, int s, int e);
int main()
{
int num[] = {
0,5,2,7,6,5,8,9,8,3,58 }; //为了使下标和堆结点对应,所以从第二项开始,第一项初始化为0
int len = sizeof(num) / sizeof(num[0]);
sort(num, len);
for (int i = 1; i < len; i++)
{
printf("%d ", num[i]);
}
return 0;
}
void sort(int* a, int len)
{
int j;
for (j = (len-1) / 2; j > 0; j--)
{
adjust(a, j, len - 1);
}
for (j = len - 1; j > 0; j--)
{
swap(a, 1, j);
adjust(a, 1, j - 1);
}
}
void swap(int* a, int b, int c)
{
int tem = a[b];
a[b] = a[c];
a[c] = tem;
}
void adjust(int* a, int s, int e)
{
int tem = a[s];
for (int j = 2 * s; j <= e; j *= 2)
{
if (a[j] < a[j + 1] && j < e) j++;
if (tem >= a[j]) break;
a[s] = a[j];
s = j;
}
a[s] = tem;
}
四.算法分析
堆排序的效率在数据量较多的情况下效率是非常高的,但数据较少时没有什么优势。
它的运行时间主要消耗在堆的初始构建和重建上
在构建堆的过程中,我们是从完全二叉树的最下层最右边的非终端结点开始构建,将它与其孩子进行比较和有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换,因此整个构建堆的时间复杂度是O(n)。
在正式排序时,第i次重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为(log2 i)+1).并需要重建i-1次,因此重建堆的时间复杂度为O(nlogn)。
由于推排序对原始数据的排序状态不敏感,所以无论是最好、最坏、平均时间复杂度都为O(nlogn).
空间复杂度上,只有一个用来交换的暂存单元,不过数据的比较和交换是跳跃进行,因此它也是不稳定的排序算法。