目录
首先介绍什么是堆,以及大(小)根堆和最大(小)优先队列;然后介绍堆排序算法;最后给出堆排序的C代码和测试脚本。
1 堆
1.1 堆的定义
(二叉堆)是一个数组,它可以被看成一个近似完全的二叉树,树上的每一个节点对应数组中的一个元素。除了底层外,该树是完全充满的,而且是从左向右填充。如图1(a)所示的二叉树可以使用图1(b)的二叉堆存储。
当二叉堆中第一个元素的下标为0时,那么对于任意一个节点:
左孩子:
右孩子:
父亲:
对于一个d叉堆,其孩子节点的范围是,父亲为。(注:这里的堆中的第一个元素下标为0,而书中第一个元素下标为1,因而给出的孩子和父亲的结论不同)。
1.2 大根堆、最大优先队列
在堆结构中,如果除根节点外所有节点都满足 ,称这个堆是一个大根堆。图1中展示的就是一个大根堆。从大根堆的定义中可以看到,任何一个子树中,子树的根节点是这个子树的最大节点。最大优先队列是大根堆的一个应用,集合S是满足大根堆特性的一个最大优先队列,其具有以下4中操作:
-
INSERT(S, i): 把节点i插入到集合S中
-
MAXIMUN(S): 返回S中具有最大值的元素
-
EXTPACT-MAX(S): 去掉S中具有最大值的元素
-
INCREASE-KEY(S, i, k): 将节点i的值增加到k
上述4种操作中,第2条MAXIMUN(S)只需要返回A[0]的值即可,这个操作不会改变最大优先队列原有的结构,时间复杂度。而1、3、4都会影响到最大优先队列的结构,可以在的时间复杂度下完成操作。
INCREASE-KEY(S, i, k): 将一个元素的值增加到k,最大优先队列在设计时只允许一个关键字增加而不允许其减小。首先将这个元素的值替换,然后循环比较节点i和i的父节点,如果i值大,则与父节点交换,否则结束循环。将图1所示最大优先队列中节点9的值增加到15,其维护过程如图2:
在上述过程中,插入替换节点耗时,调整位置时,每次比较上升一层,最坏的情况比较次,所以时间复杂度为。伪代码如下:
INCREASE-KEY(S, i, key)
if key < S[i]
err "new key is smaller than current key"
S[i] = key
while i > 0 and S[PARENT(i)] S[i]
exchange S[i] with S[PARENT(i)]
i = PARENT[i]
INSERT(S, i): 把元素i插入到集合S中。插入一个元素可以分成2步:首先在队列的最后加入一个最小的值,因为一定小于它的父节点,所以不会影响到最大优先队列的性质。然后在调用INCREASE-KEY函数,将增加到i.key。增加一个节点时间复杂度为,INCREASE-KEY的时间复杂度为,所以整体的时间复杂度为。
EXTPACT-MAX(S): 去掉S中具有最大值的元素。最大优先队列只能从根节点删除(弹出)一个节点。当根节点被删除时,其大根堆性质遭到了破坏·,需要调用方法MAX-HEAPIFY(A, i)来维护堆的性质。MAX-HEAPIFY在调用时,我们假设i节点的左孩子和右孩子都是大根堆,但节点i可能小于它的某个孩子,从而破坏大根堆的性质。在调用MAX-HEAPIF(A, i)时,比较i和它的左右孩子,如果i小于它的孩子,则max(i.right_child, i.left_child)与i交换。交换可能造成左/右子树大根堆性质遭到破坏,递归调用MAX-HEAPIFY,直到i大于它的左右节点时停止。MAX-HEAPIFY(A, i)伪代码如下:
MAX-HEAPIFY(A, i)
l = LEFT(i)
r = RIGHT(i)
if l <= heap-size and A[l] > A[i]
largest = l
else largest = i
if r <= heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)
MAX-HEAPIFY每次递归时,较上一次下降一层,因此最多调用次,时间复杂度。
EXTPACT-MAX(S)分3步:① 返回A[0]节点的值; ② 将A[0]赋值为A[heap-size]; ③ 调用MAX-HEAPIFY(A, 0)对大根堆的性质进行维护。①②两步的时间都是,所以EXTPACT-MAX(S)的时间复杂度与MAX-HEAPIFY相同为。
2 堆排序
堆排序满足空间原址性和稳定性。空间原址性:任何时候都只需要常数个额外空间存放临时数据。稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,经过排序,这些记录的相对次序保持不变。
对于一个大根堆,每调用一次EXTPACT-MAX,得到当前堆中的最大数字,堆的长度从堆末尾减1。如果将EXTPACT-MAX得到的数填充到堆末尾减少的位置,那么经过n-1次操作后,便实现了将大根堆从小到大的原址排序。EXTPACT-MAX的时间复杂度时,需要进行n - 1次操作,所以总时间复杂度。
那么堆排序需要解决的问题就只剩下如何将输入的数构成一个二叉堆——建堆。下面提供2中建堆的方法。
第一种:从输入数组的第二个元素A[1]开始,直到最后一个元素,循环调用INSERT(S, i),把每一个元素插入到堆中。INSERT的时间复杂度为,有n-1个元素需要插入,时间复杂度非紧确上界为。
第二种:自底向上的使用过程MAX-HEAPIFY把一个大小为size的数组转换为大根堆。当用数组表示存储n个元素的堆时,叶子结点的下标分别为。因此从n/2 - 1到0对每个结点调用MAX-HEAPIFY即可将输入数组转换为大根堆。MAX-HEAPIFY时间复杂度为,需要调用n/2次,所以粗略的估算其非紧确上界为(实际上时间复杂度为,但粗略估算值并不会影响堆排序的整体时间复杂度计算)。伪代码如下:
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.heap-size - 1 to 1
exchange A[0] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 0)
3 C代码与测试
3.1 堆排序C代码
/*
MAX-HEAPIFY(A, i)
l = LEFT(i)
r = RIGHT(i)
if l < A.heap-size and A[i] < A[l]
largest = l
else largest = i
if r < A.heap-size and A[largest] < A[r]
largest = r
if larget != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)
*/
#define LEFT(i) (2 * (i) + 1)
#define RIGHT(i) (2 * (i) + 2)
#define exchange(p1, p2) \
do { \
int tmp; \
tmp = *p1; \
*p1 = *p2; \
*p2 = tmp; \
} while(0)
int max_heapify(int A[], int i, int size)
{
int l, r, largest;
l = LEFT(i);
r = RIGHT(i);
if (l < size && A[i] < A[l]) {
largest = l;
} else {
largest = i;
}
if (r < size && A[largest] < A[r]) {
largest = r;
}
if (largest != i) {
exchange(&A[i], &A[largest]);
max_heapify(A, largest, size);
}
return 0;
}
/*
BUILD-MAX-HEAP(A)
for i = (A.heap-size / 2 - 1) downto 0
MAX-HEAPIFY(A, i)
*/
int build_max_heap(int A[], int size)
{
int i;
for (i = size / 2 - 1; i >= 0; i--) {
max_heapify(A, i, size);
}
return 0;
}
/*
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.heap-size - 1 to 1
exchange A[0] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 0)
*/
int heap_sort(int A[], int size)
{
int i;
build_max_heap(A, size);
for (i = size - 1; i >= 0; i--) {
exchange(&A[i], &A[0]);
size --;
max_heapify(A, 0, size);
}
return 0;
}
int sort(int n, int A[])
{
heap_sort(A, n);
return 0;
}
3.2 测试代码
程序输入参数为待排序数组,输出为排序后结果,main函数如下;
#include <stdio.h>
#include <stdlib.h>
extern int sort(int num, int arr[]);
int get_number(int num, int **arr, char *argv[])
{
int i;
*arr = (int *)malloc(sizeof(int) * num);
if (NULL == *arr) {
printf("Error: malloc failed\n");
return -1;
}
for (i = 0; i < num; i ++) {
(*arr)[i] = atoi(argv[i + 1]);
}
return 0;
}
int print_number(int num, int arr[])
{
int i;
for (i = 0; i < num; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
int main(int argc, char *argv[])
{
int num, *arr;
num = argc - 1;
if (get_number(num, &arr, argv)) {
return -1;
}
if (sort(num, arr)) {
return -1;
}
print_number(num, arr);
free(arr);
return 0;
}
使用python脚本对程序进行测试,测试脚本如下:
import os
import random
def do_test(func_str, arg, res):
cmd = func_str + " " + arg
ret = os.popen(cmd).read().strip()
if res != ret:
print "Error case\n" + "case: " + arg
print "res: " + ret
return -1
return 0
MAX = 2147483647
MIN = -2147483648
# test 10000 times
TIMES = 10000
# we will input 'number_len', which is between 1 and MAX_NUMBER, numbers into the sort program
MAX_NUMBER = 10000
if __name__ == '__main__':
func = "./sort"
for i in range(TIMES):
number_len = random.randint(1, MAX_NUMBER)
# get input number array
number = [random.randint(MIN, MAX) for i in range(number_len)]
# get sorted input number array
number_s = sorted(number)
# exchange number to string, as input srting
arg = ' '.join(str(n) for n in number)
# exchange sorted number to string, as result
res = ' '.join(str(n) for n in number_s)
# do test
ret = do_test(func, arg, res)
if ret == -1:
break
if ret != -1:
print "test success"