堆排序(Heap Sort):就是利用堆(假设利用大顶堆)进行排序的方法。
堆排序可以看成是简单选择排序的一种升级版本。
为了弄清楚堆排序,首先来学习几个概念,分别是堆、大顶堆、小顶堆
堆是具有下列性质的完全二叉树:每个节点的值都大于或者等于其左右孩子节点的值,称为大顶堆;或者每个节点的值都小于或者等于其左右孩子节点的值,称为小顶堆
可见,根结点一定是堆中所有结点最大或者最小者,如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
ki>=k2i 且 ki>=k2i+1或者 ki<=k2i 且 ki<=k2i+1 其中 (1<=i<=⌊n/2⌋)
下标i与2i和2i+1是双亲和子女关系。
基本思想:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的 n-1 个序列重新构造成一个堆,这样就会得到 n 个元素中的次大值。如此反复执行,便能得到一个有序序列了。
有了上面的基本思想的指导,接下来要解决的第一件事情就是,如何使用已经给出的数据构造一个大顶堆?
当然有办法,观察一下完全二叉树可以看到,只要把每个拥有孩子的结点形成的子树排成大顶堆,那么整个树就可以构成大顶堆,我们按照自下而上、自右向左的顺序选择结点,分别构造大顶堆即可。大概的构造过程如下图所示(图非本人绘制):
堆排序代码如下:
#include
int Mycount = 0;
using namespace std;
void swap(int k[], int i, int j)
{
int temp;
temp = k[i];
k[i] = k[j];
k[j] = temp;
}
void HeapAdjust(int k[], int s, int n)
{
int i, temp;
temp = k[s];
for (i = 2 * s; i <= n; i *= 2)
{
Mycount++;
if (i < n && k[i] < k[i + 1])
{
i++;
}
if (temp >= k[i])
{
break;
}
k[s] = k[i];
s = i;
}
k[s] = temp;
}
void HeapSort(int k[], int n)
{
int i;
for (i = n / 2; i > 0; i--)
{
HeapAdjust(k, i, n);
}
for (i = n; i > 1; i--)
{
swap(k, 1, i);
HeapAdjust(k, 1, i - 1);
}
}
int main()
{
int i;
int a[10] = { -1, 5, 2, 6, 0, 3, 9, 1, 7, 4 };
//int a[10] = { 1, 0, 2, 3, 4, 5, 6, 7, 8, 9 };
//int a[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
cout << "排序前的数组是:";
for (i = 0; i < 10; i++)
{
cout << a[i];
}
cout << endl;
HeapSort(a, 9);
cout << "总共执行" << Mycount << "次比较!" << endl;
//printf("总共执行 %d 次比较!", count);
//printf("排序后的结果是:");
cout << "排序后的结果是:";
for (i = 0; i < 10; i++)
{
cout << a[i];
}
cout << endl;
return 0;
}
执行结果:
堆排序算法的复杂度分析:
在构建堆的过程中,只需要将结点与孩子进行比较,若有必要则进行互换,因此整个构建堆的时间复杂度为O(n)
在正式排序时,因为完全二叉树的深度为,所以重建堆的时间复杂度为O(nlogn)
所以堆排序的时间复杂度为O(nlogn)
堆排序算法的稳定性:
由于堆排序的比较和交换都是跳跃式的,因此堆排序是一种不稳定的排序算法。