第六章 堆排序
堆的定义:
堆就是使用二叉树的结构来维护的一位数组。
如图,给定一个数组A,可以用一个近似完全二叉树表示。树的根节点是A[1],这样给定一个结点的下标,很容易计算得到它的父节点、左孩子、右孩子的下标:
PARENT(i)
return i/2
LEFT(i)
return 2*i
RIGHT(i)
return 2*i+1
在大多数计算机上,通常将的值左移一位表示
,右移一位表示
。在堆排序中,这三个函数通常是以“宏”或者“内联函数”的方式实现。下面我们用宏实现这三个函数:
#define PARENT(i) (i)>>1
#define LEAF(i) (i)<<1
#define RIGHT(i) ((i)<<1)+1
二叉堆可以分为两种形式:最大堆和最小堆。
最大堆需要满足,也就是某个节点必须小于等于它的父节点。
最小堆则相反,满足,某个节点必须大于等于它的父节点
维护堆的性质
MAX-HEAPIFY是用于维护最大堆性质的重要过程。它的输入为一个数组A和一个下标。在调用MAX-HEAPIFY时,我们假定根节点为LEFT(i)和RIGHT(i)的二叉树都是最大堆,但这时A[i]有可能小于其孩子,这样就违背了最大堆的性质。MAX-HEAPIFY通过让A[i]的值在最大堆中 “逐级下降”,从而保证最大堆的性质。实现过程如下:
void MAX_HEAPIFY(int *A, int len, int i) //len表示数组的长度
{
int r = RIGHT(i);
int l = LEAF(i);
int largest = i;
if(l <= len && A[l-1] > A[largest-1])
largest = l;
if(r <= len && A[r-1] > A[largest-1])
largest = r;
if(i != largest)
{
int temp = A[i-1];
A[i-1] = A[largest-1];
A[largest-1] = temp;
MAX_HEAPIFY(A, len, largest);
}
}
调用MAX_HEAPIFY(A, 10, 2)的算法过程如下:
建堆
我们可以用自低向上的方法利用过程MAX-HEAPIFY把一个大小为的数组
转换成最大堆。由于子数组
中的元素都是树的叶子结点。每个叶子结点都可以看成只包含一个元素的堆。过程BUILD-MAX-HEAP对树中的其他节点都调用一次MAX-HEAPIFY,就可以保证整棵树都是最大堆。实现过程如下:
void BUILD_MAX_HEAP(int *A, int len) //len表示数组A的长度
{
for(int i = len/2; i > 0; i--)
{
MAX_HEAPIFY(A, len, i);
}
}
堆排序
初始时候,利用BUILD-MAX-HEAP将输入数组建成最大堆。因为A[1]肯定是最大值,所以将A[1]与A[n]互换;再调用MAX-HEAPIFY将
建成最大堆,将A[1]与A[n-1]互换,;重复此过程,直到堆的大小从n-1降到2。算法实现:
void HEAPSORT(int *A, int len)
{
for (int i = 1; i < len; ++i)
{
int temp = A[0];
A[0] = A[len - i];
A[len - i] = temp;
MAX_HEAPIFY(A, len - i, 1);
}
}
图解:
堆排序的所有实现代码:
#include <iostream>
#define PARENT(i) (i)>>1
#define LEAF(i) (i)<<1
#define RIGHT(i) ((i)<<1)+1
void MAX_HEAPIFY(int *A, int len, int i)
{
int r = RIGHT(i);
int l = LEAF(i);
int largest = i;
if(l <= len && A[l-1] > A[largest-1])
largest = l;
if(r <= len && A[r-1] > A[largest-1])
largest = r;
if(i != largest)
{
int temp = A[i-1];
A[i-1] = A[largest-1];
A[largest-1] = temp;
MAX_HEAPIFY(A, len, largest);
}
}
void BUILD_MAX_HEAP(int *A, int len)
{
for(int i = len/2; i > 0; i--)
{
MAX_HEAPIFY(A, len, i);
}
}
void HEAPSORT(int *A, int len)
{
for (int i = 1; i < len; ++i)
{
int temp = A[0];
A[0] = A[len - i];
A[len - i] = temp;
MAX_HEAPIFY(A, len - i, 1);
}
}
int main()
{
int A[10] = {4, 1, 3, 2, 16, 9, 10, 14, 8, 7};
BUILD_MAX_HEAP(A, 10);
HEAPSORT(A,10);
for(int i = 0; i < 10; ++i)
std::cout<<A[i]<<" ";
return 0;
}
优先队列
优先队列(priority queue)是一种用来维护由一组元素构成的集合S的数据结构,其中每个元素都有一个相关的值,称为关键字(key)。一个最大优先队列支持一下操作:
INSERT(S,x):把一个元素插入集合S中。
MAXIMUM(S):返回S中具有最大键字的元素。
EXTRACT-MAX(S):去掉并返回S中的具有最大键字的元素。
INCREASE-KEY(S, x, k):将元素x的关键字值增加到k,这里假设k的值不小于x的原关键字值。
相应的,最小优先队列也支持上述操作,只是返回的不是最大键字的元素,而是最小键字的元素。显然,优先队列可以用堆来实现,下面我们来讨论用堆如何实现最大优先队列的操作。
过程HEAP-MAXIMUM可以在时间内实现MAXMUM操作。伪代码:
HEAP-MAXIMUM(A)
return A[1]
过程HEAP-EXTRACT-MAX实现EXTRACT-MAX操作,其实就是堆排序HEAPSORT过程中的for循环中步骤——把第一个元素与最后的元素交换,然后在重新调用MAX-HEAPIFY使得保持最大堆的性质,时间复杂度为。伪代码:
HEAP-EXTRACT-MAX(A)
if A.heap-size<1
error "heap underflow"
max=A[1]
A[1]=A[A.heap-size]
A.heap-size=A.heap-size-1
MAX-HEAPIFY(A,1)
return max
过程HEAP-INCREASE-KEY能够实现INCREASE-KEY操作。我们将A[i]的值更新时,可能违反了最大堆的性质。因此,我们需要将A[i]不断的与父节点比较,直到其小于父节点为止。时间复杂度为,伪代码:
HEAP-INCREASE-KEY(A,i,key)
if key < A[i]
error "new key is smaller than current key"
A[i] = key
while i>1 and A[PARENT(i)]<A[i]
exchange A[i] with A[PARENT(i)]
i=PARENT(i)
实现过程:
MAX-HEAP-INSERT能够实现INSERT操作。它的输入是要在最大堆中插入一个新的元素,算法的实现过程是:首先通过增加一个关键字来扩展最大堆,之后调用上面的HEAP-INCREASE-KEY为新节点设置对应的关键字,同时保持最大堆的性质。时间复杂度为
。伪代码:
MAX-HEAP-INSERT(A,key)
A.heap-size = A.heap-size+1
A[A.heap-size]=-∞
HEAP-INCREASE-KEY(A,A.heap-size,key)
总之,在包好n个元素的堆中,所有优先队列的操作都可以在时间内完成。