本章堆排序内容是《算法导论》教材第二部分《排序与顺序统计量》的第一讲。
堆排序,这是一种O(nlgn)时间的原址排序算法。它使用了一种被称为堆的数据结构,堆还可以用来实现优先级队列。
数组R[1...n]中,n个关键字序列k1,k2,…,kn,当且仅当该序列满足如下性质(简称为堆性质,以大根堆为例):
二叉堆是一个数组,它可以被看成是一个近似的完全二叉树。树上的每一个结点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。表示堆的数组A包括两个属性:A.length表示数组元素的个数;A.heap_size表示数组A有具有堆结构的元素个数。有:0=<A.heap_size=<A.length。A[1]是树的根节点。
通常给定节点i,我们很容易得到可以根据其在数组中的位置求出该节点的父亲节点、左右孩子节点:
#define PARENT(i) i/2 #define LEFT(i) i*2 #define RIGHT(i) i*2+1
根据节点数值满足的条件,可以将分为最大堆和最小堆,最大堆和最小堆除了需要满足堆的性质外还要满足:
- 含有n个元素的堆的高度是lgn;
- 堆结构上的基本操作的运行时间与树高成正比,即时间复杂度为:O(nlgn);
- 当用数组表示存储了n个元素的堆时,最后一个非叶子节点是:n/2;叶子节点的下标是:n/2+1,n/2+2,……,n;
- 在最大堆中,最大元素该子树的根上;在最小堆中,最小元素在该子树的根上。
这里讲的维护堆的性质,主要是指维护堆当前节点的值都要大于左子节点值和右子节点值。当堆中数据变动时,通过维护函数能够调整树的结构中元素的位置,以使数组还能继续满足堆的性质。
这里我们构造一个函数:max_heapify()用来维护最大堆性质的重要过程。在调用max_heapify()的时候,我们假定根节点为LEFT(i)和RIGHT(i)的二叉树都是最大堆。max_heapify()就是通过让A[i]的值在最大值中“下沉”,从而使得以下标i为根节点的子树重新满足最大堆的性质。
图 2 max_heapify()函数调整i=2位置元素的执行过程
从上图中可以看出,A[2]违背了最大堆的性质,因为它的值不大于它的孩子,所以需要进行调整。根据教材中max_heapify()的伪代码的代码结构,我进行了C++实现的递归实现,函数max_heapify()的返回值为void:
//维护堆的性质:即使得结点i出左右子树的值小于该节点的值 void max_heapify(int *arr, int length, int i) { int left, right, largest; int temp; left = LEFT(i); right = RIGHT(i); if (left <= length && arr[left] > arr[i]) largest = left; else largest = i; if (right <= length && arr[right] > arr[largest]) largest = right; if (i != largest) { temp = arr[i]; arr[i] = arr[largest]; arr[largest] = temp; max_heapify(arr, length, largest); } }max_heapify()函数的时间代价包括:
build_max_heap(A) 1 A.heap_size=A.length 2 for i = A.length/2 downto 1 3 max_heapify(A,i)C++代码实现:
//创建堆 void build_max_heap(int *arr, int length) { int i; for (i = length / 2; i > 0; i--) { max_heapify(arr, length, i); } }证明build_max_heap()的正确性:
初始化:第一次迭代时:i=length/2,其他节点length/2 +1,length/2 +2,......n都是叶结点。因此节点i=1,......length/2,都是平凡最大堆的根节点。
保持:在for循环中,i的值依次递减,依次建立节点length/2-1,...... ,1的堆结构。
终止:当i=0时,退出for循环。
HEAPSORT(A) 1 BUILD_MAX_HEAP(A) 2 for i = A.length downto 2 3 exchange A[1] with A[i] 4 A.heap_size = A.heap_size - 1 5 MAX_HEAPIFY(A,1)C++代码实现:
//堆排序 void heap_sort(int *arr, int length) { int i, temp; build_max_heap(arr, length); i = length; while (i > 1) { temp = arr[i]; arr[i] = arr[1]; arr[1] = temp; i--; max_heapify(arr, i, 1); } }下图显示了HEAPSORT(A)伪代码第2~第5行for循环第一次迭代开始前最大堆的情况和每一次迭代后最大堆的情况:
#include <iostream> using namespace std; #define PARENT(i) i/2 #define LEFT(i) i*2 #define RIGHT(i) i*2+1 //维护堆的性质:即使得结点i出左右子树的值小于该节点的值 void max_heapify(int *arr, int length, int i) { int left, right, largest; int temp; left = LEFT(i); right = RIGHT(i); if (left <= length && arr[left] > arr[i]) largest = left; else largest = i; if (right <= length && arr[right] > arr[largest]) largest = right; if (i != largest) { temp = arr[i]; arr[i] = arr[largest]; arr[largest] = temp; max_heapify(arr, length, largest); } } //创建堆 void build_max_heap(int *arr, int length) { int i; for (i = length / 2; i > 0; i--) { max_heapify(arr, length, i); } } //堆排序 void heap_sort(int *arr, int length) { int i, temp; build_max_heap(arr, length); i = length; while (i > 1) { temp = arr[i]; arr[i] = arr[1]; arr[1] = temp; i--; max_heapify(arr, i, 1); } } int main() { int arr[11] = {NULL, 13, 45, 32, 5, 21, 0, 23, 78, 90, 12}; cout << "----------------堆排序--------------------" << endl; cout << "排序前:"; for (int i = 1; i < 11; i++) cout << arr[i] << " "; cout << endl; heap_sort(arr, 10); cout << "排序后:"; for (int i = 1; i < 11; i++) cout << arr[i] << " "; cout << endl; system("pause"); return 0; }程序运行结果: