既然要说堆排序,那么自然先问的是什么是堆?
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序(Heap)是利用堆(假设利用大顶堆)进行排序的方法。
知道了堆和堆排序的概念之后,那么怎么实现堆排序呢?
目录
一、堆排序的基本思想是:
(1)将待排序的序列构造成一个 大顶堆。此时,整个序列的最大值就是堆顶的根节点。
(2)将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
上面说了这么多,无非就是想解决两个问题:
(1)如何由无序序列构建成一个堆
(2)如何在输出堆顶元素以后,调整剩余元素称为一个新堆
(如果对理论不感兴趣的,可以直接看实现了 :) ^_^
说到这里,就不得不提起二叉树的往事了,二叉树呢,有这么一个性质:
若完全二叉树中一共n个数,那么非根节点的个数一定等于 个,其中 表示向下取整;
因为,第i个结点的左孩子等于 2*i, 右孩子等于 2*i +1 (假设根节点是 i = 1); 通过对这 个结点分析大顶堆的构造,可以从序号最大的结点(层次最深的)开始,然后,逐个往序号小的结点(层次较小的)0 构造。所以,此处构造堆只需要3个数之间,两次比较即可,构造耗时呢,也就2*,即O(n)时间复杂度。最后,再经过n-1次正式排序,每次需要logn的复杂度。
因此,总得来说,堆排序的时间复杂度为O(nlogn),由于比较和交换是跳跃式进行的,因此堆排序也会是一种不稳定的排序方法。
厉害了,就这么一个性质,够用。
二、堆排序的C++实现
#include <iostream>
#include <algorithm>
using namespace std;
//在数组a[s...m]中,认为除了a[s]以外,其余结点满足大顶堆的条件
//通过调整a[s]的关键字,使得a[s...m]都满足大顶堆的条件
//下面是利用大顶堆实现顺序排序
void adjustheap(int *a, int s, int m)
{
int temp = a[s];
int j;
for (j = 2 * s +1 ; j < m ; j = 2 * j + 1)
{
if (j+1 < m && a[j] < a[j+1])
j++;
if (a[j] <= temp) //大顶堆已经确定,就是a[temp]
break;
a[s] = a[j]; //将较大值放在大顶堆上
s = j; //沿着比较大的支流接着寻找
}
a[s] = temp; //插入到找到的位置
}
void heapsort(int *a, int length)
{
int i;
//1.大顶堆的构造
for (int i = (length-1 - 1) / 2; i >= 0; i--)
adjustheap(a, i, length);
//2.输出堆顶元素,重新构造剩余的元素为大顶堆
for (int i = length - 1; i > 0; i--)
{
swap(a[i], a[0]); //第一个位置一定是大顶堆的堆顶
adjustheap(a, 0, i);
}
}
int main(int argc, char **argv)
{
int a[] = { 1,3,5,6,7,2,10,8 };
int b[] = { 90, 70, 80, 60 ,10 , 40, 50, 30, 20 };
//swap(a[0], a[1]);
heapsort(b, sizeof(b) / sizeof(int));
for (int i = 0; i < sizeof(b) / sizeof(int); i++)
cout << b[i] << " ";
cin.get();
return 0;
}
其实,在上面的实现过程中,根节点是序号0,
1.如果根节点是i =0 开始的,那么满足下面的二叉树序号关系
i
2*i+1 2*i+2
此时,最大序号为n-1的二叉树,非根节点的个数就等于 -1。
所以,在adjustheap中看到的也是 2*i+1和2*i+2的调整
2.如果根节点是i = 1开始的,那么满足下面的二叉树序号关系
i
2*i 2*i+1
此时,最大序号为n的二叉树,非根节点的个数就等于 。
这种情况下,a[0]往往作为前哨,待排序数组的元素从a[1]开始放置。那么,相应的,在adjustheap中看到的也是 2*i和2*i+1的调整。
最后,如果要实现逆序排序,那么可以用小顶堆实现。