在之前的算法导论的学习当中,我们学习了归并排序算法和插入排序算法。这里我们讨论算法导论第六章中的内容,堆排序。和归并排序相同的这个算法的时间的复杂度都是O(nlgn),和插入排序算法相同的是——堆排序是原址排序的。堆排序是一种十分优秀的算法,但是在实际的生产环境当中,快速排序算法使用更加频繁。快速排序的结构更加紧凑,因而时间复杂度之前的系数因子比较小,而且堆排序的实现比快速排序要更加复杂一些,在堆排序的实现中有更多的东西需要注意。
首先介绍一下堆的概念,堆在某种程度上可以理解为一个树,只不过每个节点并不是特定的数据结构,而是数组中的一个元素。具体解释可以看书上的图解。既然可以认为是一棵树,那么就有根节点,父节点,左子节点,右子节点的概念,当然这些树中的概念是很相似的。从逻辑上我们完全可以把堆当作树来处理,而从物理存储上我们却可以把堆当作数组来处理。下面是我对堆这个数据结构的定义。
typedef int ELeType;
typedef struct heap {
ELeType arr[MAXSIZE];
int heap_size;
}HEAP;
其中heap_size很重要,这个字段记录了heap中存储的有效的(或者说我们想要处理的)元素有多少。MAXSIZE这个是一个常数。
这棵“树”的根节点是下标为1的数组元素,(在这里我按照书上的定义方式,数组从下标1开始,但是在c语言的具体实现当中,我不得不为每个下标减去1)。它的左孩子的下表2,右孩子的下标为3......第i个左孩子和右孩子分别为2*i和2*i+1,所以我们可以非常轻松地理解下面的代码。
int parent (int i) {
return i/2;
}
int left_child (int i) {
return i*2;
}
int right_child (int i) {
return (i*2+1);
}
有了这些之后我们就可以写一个非常重要的函数了——维持最大堆的性质。当然,这个函数有一个非常重要的前提——假设它的左子堆和右子堆都已经是最大子堆了。
void max_heapify (HEAP *heap , int start) {
int max_pos = start;
int left_pos = left_child (start);
int right_pos = right_child (start);
if (start < heap->heap_size) {
if (left_pos <= heap->heap_size && heap->arr[max_pos - 1] < heap->arr[left_pos - 1]) {
max_pos = left_pos;
}
if (right_pos <= heap->heap_size && heap->arr[max_pos - 1] < heap->arr[right_pos - 1]) {
max_pos = right_pos;
}
}
if (max_pos != start) {
swap (&heap->arr[start - 1] , &heap->arr[max_pos - 1]);
max_heapify (heap , max_pos);
}
}
这里针对heap堆中的i节点,使其以i节点的子树可以成为最大堆,当然需要重复的是,这里假设这个i节点的左子树和右子树都已经是最大子树了。这里我们看见函数中最后一句话是递归调用语句,这里的作用就是,由于再调换当前的元素的值之后,还不能保证是最大字数,因为之前的i节点中的值可能仍然小于交换位置之后的孩子节点的值。所以需要递归。
有了这个重要的支撑之后,我们就可以建立最大堆了。下面是最大堆的算法。
void build_max_heap (HEAP *heap) {
int n = heap->heap_size;
int i;
for (i = n / 2 ; i >= 1 ; i--) {
max_heapify (heap , i);
}
}
只需要知道堆从哪个下标开始是叶节点这个算法就可以很容易的理解。因为叶节点已经是最大子堆了。
//when we run this function heap must be a max heap! (we should use build_max_heap before this)
void heap_sort (HEAP *heap) {
int i;
int n = heap->heap_size;
for (i = n ; i >= 2 ; i--) {
swap (&heap->arr[i-1] , &heap->arr[0]);
heap->heap_size--;
max_heapify (heap , 1);
}
}
这里需要注意的一点是,heap必须是一个最大子堆,必须在调用这个函数之前调用build_max_heap函数。