堆排序
堆排序是利用堆(Heap)来进行排序的一种方法,在堆排序中,我们需要引入一种叫做“堆”的数据结构。
堆一般可以看做是一棵完全二叉树,且堆中某个节点的值总是不大于或不小于其父节点的值,前者我们称之为最大堆,后者称之为最小堆。
原理
堆排序的思想是先用所有元素建立一个最大堆(当然,这里用最小堆也是可以的,方法类似最大堆,不失一般性,就只讲最大堆了),然后每次取出最大的那个元素,并把剩下的元素再次调整为一个最大堆,如此循环。步骤如下:
- 用所有元素建立一个最大堆。
- 取出最大的元素,将其与堆中的最后一个元素交换。
- 因为已经取出了一个元素,将堆的大小减一。
- 调整剩下的堆中元素重新组成一个最大堆。
- 重复2-4步,直到堆中只剩最后一个元素为止。
建堆和调整堆为最大堆都需要用到堆调整的功能,其前提是调整前只有顶点元素不满足最大堆的条件。此功能步骤如下:
- 记录顶点元素的值。
- 找到顶点、其左孩子、其右孩子三者中的最大一个,若最大者为顶点,结束;否则交换最大者与顶点。
- 对以最大者为根的子堆重复1,2步,直到结束。
注意这里第3步的实现可以用递归,也可以用循环。
实现
按照以上原理我们来用代码实现。
下面就是用C语言实现的代码。分成两个函数来实现。
- 要排序的数组a有n个元素。
- heap_sort 先建最大堆,然后每次取出最大的那个元素a[0]放入已排序区,再调用 heap_adjust 调整剩下的堆为最大堆。
- heap_adjust 的输入中p、r分别指向堆的第一个元素和最后一个元素。每次找到父亲节点、左孩子、右孩子中最大的那个,将其调整为父亲节点,然后如有需要,继续调整子堆。这里没有用递归,用了循环。
/* p、r分别指向堆的第一个元素和最后一个元素 */
void heap_adjust(int a[], int p, int r)
{
int tmp = a[p]; //p指向要调整加入的堆顶点,tmp是其值
for (int i=2*p+1; i<=r; i=2*p+1) {//a[i]为a[p]的左孩子,a[i+1]为a[p]的右孩子
if (i<r && a[i]<a[i+1]) i++; //找出两个孩子中大的一个,用i指向他
if (tmp > a[i]) break; //若父亲比最小的孩子大,则已调整好
a[p] = a[i]; //否则将最大的孩子调整为父亲,
p = i; //并将p指向要调整的子堆顶点
}
a[p] = tmp; //将要加入的元素放入调整好的位置
}
void heap_sort(int a[], int n)
{
int i;
/* 建最大堆 */
for (i=n/2-1; i>=0; i--)
heap_adjust(a, i, n-1);
/* 每次取出当前最大元素a[0],然后调整堆为最大堆 */
for (i=n-1; i>0; i--) {
swap(&a[0], &a[i]); // 将a[0]放到已排序区域
heap_adjust(a, 0, i-1); // 调整剩下的堆为最大堆
}
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
为了验证此函数的效果,加上了如下辅助代码,对3个数组进行排序,运行结果在最后,可见排序成功。
#include <stdio.h>
#include <stdlib.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void heap_sort(int a[], int n);
void show_array(int a[], int n);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
heap_sort(array1, SIZE_ARRAY_1);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
heap_sort(array2, SIZE_ARRAY_2);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
heap_sort(array3, SIZE_ARRAY_3);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
运行结果:
Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18
分析
时间复杂度
从代码可见,heap_adjust 的过程是把二叉树的顶点元素往子树移动,最多移动到叶节点,即最多有 log n \log n logn 次,所以 heap_adjust 的时间复杂度为 O ( log n ) O(\log n) O(logn)。
而 heap_sort 中的 for 循环会遍历一次整个数组,量级为 n n n,for 循环中调用 heap_adjust,所以整个堆排序的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)。
空间复杂度
因为堆排序可以原址进行,所以其空间复杂度为 O ( 1 ) O(1) O(1) 。