实际中,待排序的数很少是单独的数值,它们通常是称为记录的数据集的一部分,每个记录包含一个关键字,即排序问题中要重排的值,记录的剩余部分由卫星数据组成,通常是要与关键字一同存取的。如果每个记录包含大量卫星数据,我们通常重排记录指针的数组,而非记录本身,这样可以降低数据移动量。
快排实际通常比堆排序快,虽然它们的期望运行时间都是θ(nlgn)。
插入排序、归并排序、堆排序、快速排序都是比较排序,任意比较排序算法排序n个元素的最坏情况运行时间的下界为Ω(nlgn),因此堆排序和归并排序是渐进最优的比较排序算法。
如果通过比较操作之外的方法来获得输入序列有序次序的信息,就可能打破Ω(nlgn)的下界,如计数排序假定输入元素的值范围为0~k,通过使用数组索引作为确定相对次序的工具,计数排序可以在θ(k+n)的时间内排好n个数,当k=O(n)时,计数排序算法的运行时间与输入数组的规模呈线性关系。
上表中基数排序的d表示每个数据都是d位数,k表示每个数字可能取k个值;桶排序是假定关键字是半开区间内服从均匀分布的n个实数。堆排序的平均情况运行时间未给出,本书未分析它。
一个n个数的集合的第i个顺序统计量是集合中第i小的数。
堆排序与插入排序相同,都是原址排序,但堆排序的时间复杂度与归并排序相同为O(nlgn),它集合了目前讨论的两种排序算法的优点。
(二叉)堆是一个数组,它可被看成一个完全二叉树。树的根节点是A[1],给定一个结点的下标i,可以计算出它父节点、左右孩子节点的下标:
在大多数计算机上,通过将i的值左移一位,可以在一条指令内计算出2i。
二叉堆分最大堆和最小堆,最大堆的每个节点的性质:
因此,最大堆中最大元素存放在根节点中。最小堆的每个节点的性质:
堆排序算法使用的是最大堆,最小堆通常用于构造优先队列。
定义一个堆中节点的高度为该节点到叶节点最长简单路径上边的数目。高度为h的堆中,元素个数最多为2h+1-1,元素个数最少为2h。
含n个元素的堆的高度为⌊lgn⌋。
最大堆的任一子树包含的最大元素在该子树的根节点上。
假设一个最大堆中所有元素各不相同,则该堆的最小元素放在叶子节点上,即在堆数组的后半部分。
一个从小到大已排好序的数组是一个最小堆,因为每个节点的值都小于等于它的孩子节点的值。
当用数组表示存储n个元素的堆时,如果数组下标从1开始,则叶节点下标分别是⌊n/2⌋+1、⌊n/2⌋+2、…、n。
以下过程用于维护最大堆的性质,它的输入为一个数组A和一个下标i,调用以下过程时,假定根节点LEFT(i)和RIGHT(i)的二叉树都是最大堆,但这时A[i]有可能小于其孩子,以下过程通过让A[i]的值在最大堆中逐级下降,从而使得以下标i为根节点的子树重新遵循最大堆的性质:
MAX-HEAPIFY(A, i):
l = LEFT(i)
r = RIGHT(i)
if l <= A.heap-size and A[l] > A[i]
largest = l
else largest = i
if r <= A.heap-size and A[r] > A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)
以上过程执行图解:
以上过程调整节点和其孩子节点值的时间为θ(1),假设以上过程递归调用会发生,由于每个孩子的子树大小至多为⌊2n/3⌋(最坏情况发生在根节点的左子树且最底层恰好半满的时候),因此MAX-HEAPIFY的运行时间为:
上述递归式的解为T(n)=O(lgn),即对于一个树高为h的节点,时间复杂度为O(h)。
MAX-HEAPIFY的递归调用可能使某些编译器产生低效的代码,使用循环重写MAX-HEAPIFY代码:
MAX-HEAPIFY(A, i):
while true
l = LEFT(i)
r = RIGHT(i)
if l ≤ A.heap-size and A[l] > A[i]
largest = l
else largest = i
if r ≤ A.heap-size and A[r] > A[largest]
largest = r
if largest == i
return
exchange A[i] with A[largest]
i = largest
我们可以用自底向上的方法利用MAX-HEAPIFY把一个大小为n=A.length的数组A转换为最大堆,从第一个有叶子节点的节点开始向上调整即可:
BUILD-MAX-HEAP(A):
A.heap-size = A.length
for i = ⌊A.length / 2⌋ downto 1
MAX-HEAPIFY(A, i)
以上过程的时间复杂度为O(nlgn),因为要调用O(n)次MAX-HEAPIFY,但这个结果不是渐近紧确的,渐近紧确的时间代价为O(n),即可以在线性时间内将一个无序数组构造成一个最大堆。
对于任一包含n个元素的堆,最多有⌈n/2h+1⌉个高度为h的节点。
堆排序算法利用BUILD-MAX-HEAP将输入数组建成最大堆,因为数组中的最大值在根节点中,通过交换根节点的值与数组中最后一个位置的值,可将根节点元素放到正确的位置(即从小到大排序的最后一个位置)。之后调用MAX-HEAPIFY(A, 1)即可重新构造一个最大堆:
HEAPSORT(A):
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
以上过程的时间复杂度为O(nlgn)。
C++实现以上排序过程:
#include <iostream>
#include <vector>
#include <limits>
using namespace std;
template<typename T> void heapify(vector<T>& vec, size_t i, size_t end) {
size_t l = (i << 1) + 1;
size_t r = l + 1;
while (true) {
size_t largest = i;
if (l <= end && vec[l] > vec[i]) {
largest = l;
}
if (r <= end && vec[r] > vec[largest]) {
largest = r;
}
if (largest == i) {
return;
}
swap(vec[largest], vec[i]);
i = largest;
l = (i << 1) + 1;
r = l + 1;
}
}
template<typename T> void buildHeap(vector<T>& vec) {
size_t sz = vec.size();
size_t lastNodeWithChild = sz >> 1;
for (size_t i = lastNodeWithChild; i > 0; --i) {
heapify(vec, i - 1, sz - 1);
}
}
template<typename T> T maximum(vector<T>& vec) {
return vec[0];
}
template<typename T> T extractMaximum(vector<T>& vec) {
T max = vec[0];
size_t sz = vec.size();
vec[0] = vec[sz - 1];
vec.pop_back();
heapify(vec, 0, sz - 2);
return max;
}
template<typename T> void incrKey(vector<T>& vec, size_t i, T toVal) {
if (vec[i] > toVal) {
throw exception("dec val in incrKey");
}
if (i == 0) {
return;
}
vec[i] = toVal;
size_t parent = (i - 1) >> 1;
while (vec[parent] < vec[i]) {
swap(vec[parent], vec[i]);
if (parent == 0) {
return;
}
i = parent;
parent = (i - 1) >> 1;
}
}
template<typename T> void insertKey(vector<int>& vec, T val) {
vec.push_back(numeric_limits<T>::min());
size_t sz = vec.size();
incrKey(vec, sz - 1, val);
}
// 最大优先队列的堆数组的下标从0开始
int main(){
vector<int> ivec = { 6,5,4,9,8,9,7,1,5,3,2 };
buildHeap(ivec);
for (int i : ivec) {
cout << i;
}
cout << endl;
insertKey(ivec, 8);
for (int i : ivec) {
cout << i;
}
cout << endl;
}
以上incrKey过程中,与父子节点交换一般至少需要三次交换才能完成,可优化一下:
template<typename T> void incrKey(vector<T>& vec, size_t i, T toVal) {
if (vec[i] > toVal) {
throw exception("dec val in incrKey");
}
if (i == 0) {
return;
}
vec[i] = toVal;
T key = vec[i];
size_t parent = (i - 1) >> 1;
while (vec[parent] < vec[i]) {
vec[i] = vec[parent];
if (parent == 0) {
return;
}
i = parent;
parent = (i - 1) >> 1;
}
vec[parent] = key;
}
如想删除最大优先队列中的一个节点,如果要删除的节点值比数组尾的值要大,则将要删除的节点值改为数组尾的值,再调用heapify调整子树,最后删除最后一个节点即可;如果要删除的节点值比数组尾的值小,调用incrKey将要删除的值增加为数组尾的值,最后删除最后一个节点即可:
template<typename T> void deleteKey(vector<T>& vec, size_t i) {
size_t sz = vec.size();
if (vec[i] > vec[sz - 1]) {
vec[i] = vec[sz - 1];
heapify(vec, i, sz - 1);
} else {
incrKey(vec, i, vec[sz - 1]);
}
vec.pop_back();
}
如果要合并k个有序链表,每次取k个链表中头节点元素最小的节点加入合并链表中,如果通过比较获取最小节点需要O(k)的时间,一共需要排序n个(k个链表的节点数总和)节点,需要进行n次循环,因此时间复杂度为O(nk)。但可以使用最小堆以O(lgn)的时间获取最小节点,从而将时间复杂度降低为O(nlgk)。
用以下插入的方法建堆:
BUILD-MAX-HEAP'(A):
A.heap-size = 1
for i = 2 to A.length
MAX-HEAP-INSERT(A, A[i])
当输入数据相同时,BUILD-MAX-HEAP和BUILD-MAX-HEAP’生成的堆不总是相同的,使用以上过程建堆所需时间为O(nlgn)。