优先队列
许多程序都需要处理有序的元素。但不一定要求他们全部有序,或是不一定要一次就将它们排序。很多情况下我们会收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前最大的元素,如此这般。例如,现在绝大部分电脑或是手机等电子设备都可以同时运行多个应用程序。这是通过为每个应用程序的事件分配一个优先级,并总是处理下一个优先级最高的事件来实现的。例如,绝大部分手机分配给来电的优先级都会比游戏程序高。
在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这种数据类型叫做优先队列。
堆的定义
数据结构二叉堆能够很好的实现优先队列的基本操作。注意,二叉堆指的是一个数组,在这个数组当中,每个元素都要保证大于等于另外两个特定的元素;相应的,这些位置的元素又至少要大于等于数组中另两个元素,以此类推。我们将数组中的元素想象成一个二叉树的话,这种结构会很好理解。当一棵二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序。
相应的,在堆有序的二叉树中,每个结点都小于等于它的父结点(如果有的话)。从任意结点向上,我们都能得到一列非递减的元素;从任意结点向下,我们都能得到一列非递增元素。特别地,根结点是堆有序的二叉树中最大结点。
我们可以用链表来表示堆有序的二叉树,那么每个元素都需要三个指针来找到它的上下结点(父结点和两个子结点各需要一个)。但如图所示,如果我们使用完全二叉树,表达就会特别方便。要画出这样一棵完全二叉树,可以先定下根结点,然后一层层由上至下,从左至右,在每个结点的下方连接更小的结点,直至将N个结点全部连接完毕,一个堆有序的完全二叉树只用数组而不需要链表就可以完成。二叉堆是一组能够用堆有序的完全二叉树排序的元素,按照层级顺序放入数组中(不使用数组的第一个元素)。
堆的算法
在介绍核心算法之前,我们首先做几点声明。我们用长度为N+1的数组来表示一个大小为N的堆,我们不使用数组的第0号元素,堆元素放在数组第1至第N号位置。在排序算法中,我们使用两个辅助函数less() 和 exchange() 来实现堆中元素的比较与交换,方法如代码框中所示。
int compareTo(int i, int j) {
if (pq[i] < pq[j])return -1;
if (pq[i] > pq[j])return 1;
else return 0;
}
int less(int i, int j) {
return compareTo(i, j) < 0;
}
void exchange(int i, int j) {
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
下面还有两个辅助方法叫做swim() 和 sink()。在有序化的时候会遇到两种情况:一种是当某个结点的优先级上升(或是在堆底加入一个新的元素)时,我们需要由下至上恢复堆的顺序。当某个结点优先级下降(例如,将某个元素替换成一个较小的元素)时,我们需要由上至下恢复堆的顺序。
由下至上的堆有序化(上浮)
堆的有序状态因为某个结点变得比其父结点更大而被打破,通过交换这个结点和它的父结点来修复堆。交换后,这个结点比它的两个子结点都要大,但是这个结点还有可能比它现在的父结点要大,所以我们可以一遍遍用同样的方法恢复秩序,将这个结点不断向上移动直到我们遇到一个更大的父结点,这个过程通过二叉树的性质是比较容易实现的。
void swim(int k) {
while (k > 1 && less(k / 2, k)) {
exchange(k / 2, k);
k = k / 2;
}
}
由上至下的堆有序化(下沉)
如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小而被打破,那么我们可以将该结点和它的两个子结点当中较大的一个交换来恢复堆。交换可能会在子结点处继续打破堆的有序状态,因此我们需要用同样的方法将其修复,将结点向下移动直到它的子结点都比它更小或是到达堆的底部。
void sink(int k,int N) {
while (2 * k <= N) {
int j = 2 * k;
if (j < N && less(j, j + 1))j++;
if (!less(k, j))break;
exchange(k, j);
k = j;
}
}
swim() 和 sink() 是高效的实现优先队列两个关键操作的基础,原因如下:
插入元素:我们将新元素插入到数组的末尾,增加堆的大小并让这个元素上浮到合适的位置。如图中左侧所示。
删除最大元素:我们从数组顶端删去最大元素并将数组最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。如图中右侧所示。
基于堆的优先队列
#include<iostream>
using namespace std;
class MaxPQ {
private:
int* pq;
int N;
int maxsize;
void inc();
bool isEmpty();
bool isFull();
int less(int i, int j);
int compareTo(int i, int j);
void exchange(int i, int j);
void swim(int k); //上浮
void sink(int k, int N); //下沉
public:
MaxPQ();
MaxPQ(int arr[], int n);
~MaxPQ();
MaxPQ(const MaxPQ& src);
MaxPQ& operator=(const MaxPQ& src);
int size();
void insert(int v);
int delMax();
void show();
friend void HeapSort(int arr[], int n);//将堆排序声明为友元函数
};
以上是优先队列的函数接口。该优先队列本质上是一个二叉堆,物理上是一个数组,逻辑上是一个堆有序的完全二叉树。类提供的公共接口主要是insert
和delMax
。在insert
中,我们把新元素添加到数组的末尾,然后用swim
恢复秩序。在delMax
中,获取首元素后将其替换为最后一个元素,然后用sink
恢复堆的秩序。
堆排序
基于堆的优先队列可以实现一种排序算法,叫做堆排序。这种排序使用了前文我们介绍的思想和方法,是一种非常经典的排序方法。
堆排序分成两个阶段,首先是堆的构造和下沉排序。
堆的构造
如何用N个给定元素来构造一个堆?一种方法很容易想到,只需从左向右遍历数组,用swim
方法保证指针左侧的所有元素已经是一棵堆有序的完全二叉树即可,就像连续向优先队列中插入元素一样。还有一种方法,是从右至左用sink
函数构造子堆。数组中的每一个位置都可以看做是一个子堆的根结点,那么sink
对于这些子堆也同样适用。如果一个结点的两个子结点都已经是一个堆了,那么对这个结点调用sink
方法可以将这三个结点变成一个堆。这个方法相当于递归地建立起堆的秩序。开始时我们只用扫描数组中一半的元素,因为我们可以跳过大小为1的子堆。最后我们在数组位置为1的地方调用sink
方法,扫描结束。这样构建后的数组相当于存储着一个堆有序的完全二叉树。
下沉排序
堆排序的主要工作是在第二阶段完成的。我们将堆中最大元素删除,然后放入堆缩小后数组空出的位置。这个过程与选择排序有些类似(选择排序每次在剩余元素中选择最小元素,而我们实现的堆排序每次选择的是最大元素),但所需的比较要少很多,因为堆提供了一种从未排序序列部分找到最大元素的有效方法。
以下是堆排序的算法:
void HeapSort(int arr[], int n) {
/*构建优先队列*/
MaxPQ m(arr, n);
/*下沉排序*/
int N = m.size();
while (N > 1) {
m.exchange(1, N--);
m.sink(1, N);
}
/*回写到原来的数组中*/
for (int i = 1;i <= m.size();i++)
{
arr[i - 1] = m.pq[i];
}
}
接口具体实现
有参构造函数主要用于堆排序,对任意传入的数组进行排序,使之成为优先队列;除此之外,无参构造函数适用于任何情况,配合插入函数同样可以构成优先队列。关键方法是sink
和swim
,通过它们实现的insert
和delMax
是优先队列的基础。实际可以看出,堆排序是这个类可以完成的一个典型应用。
#define DEFAULT_SIZE 10
MaxPQ::MaxPQ()
{
N = 0;
maxsize = DEFAULT_SIZE;
pq = new int[maxsize + 1];
if (pq == NULL)
{
cout << "new failed" << endl;
exit(EXIT_FAILURE);
}
}
MaxPQ::MaxPQ(int arr[], int n)
{
N = 0;
maxsize = n;
pq = new int[maxsize + 1];
if (pq == NULL)
{
cout << "new failed" << endl;
exit(EXIT_FAILURE);
}
else
{
for (int i = 1;i <= n;i++)pq[++N] = arr[i - 1];
for (int k = N / 2;k >= 1;k--)sink(k, size());
}
}
MaxPQ::~MaxPQ()
{
free(pq);
pq = NULL;
N = 0;
maxsize = 0;
}
MaxPQ::MaxPQ(const MaxPQ& src)
{
N = src.N;
maxsize = src.maxsize;
pq = new int[maxsize + 1];
if (pq == NULL)
{
cout << "new failed" << endl;
exit(EXIT_FAILURE);
}
else
{
for (int i = 1;i <= N;i++)pq[i] = src.pq[i];
}
}
MaxPQ& MaxPQ::operator=(const MaxPQ& src)
{
if (this == &src)
{
return *this;
}
N = src.N;
maxsize = src.maxsize;
int* pnew = new int[maxsize + 1];
if (pq == NULL)
{
cout << "new failed" << endl;
exit(EXIT_FAILURE);
}
else
{
for (int i = 1;i <= N;i++)pnew[i] = src.pq[i];
}
free(pq);
pq = pnew;
return *this;
}
int MaxPQ::size()
{
return N;
}
void MaxPQ::insert(int v)
{
if (isFull())
{
inc();
}
pq[++N] = v;
swim(N);
}
int MaxPQ::delMax()
{
if (isEmpty())
{
cout << "empty queue" << endl;
return -1;
}
int max = pq[1];
exchange(1, N--);
pq[N + 1] = 0;
sink(1, N);
return max;
}
void MaxPQ::show()
{
for (int i = 1;i <= N;i++) {
cout << pq[i] << " ";
}
}
void MaxPQ::inc()
{
maxsize *= 2;
int* newp = new int[maxsize + 1];
if (newp == NULL)
{
cout << "new failed" << endl;
exit(EXIT_FAILURE);
}
else
{
for (int i = 1;i <= N;i++)
{
newp[i] = pq[i];
}
pq = newp;
}
}
bool MaxPQ::isEmpty()
{
return N == 0;
}
bool MaxPQ::isFull()
{
return N == maxsize;
}
int MaxPQ::less(int i, int j)
{
return compareTo(i, j) < 0;
}
int MaxPQ::compareTo(int i, int j)
{
if (pq[i] < pq[j])return -1;
if (pq[i] > pq[j])return 1;
else return 0;
}
void MaxPQ::exchange(int i, int j)
{
int temp;
temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
void MaxPQ::swim(int k)
{
while (k > 1 && less(k / 2, k)) {
exchange(k / 2, k);
k = k / 2;
}
}
void MaxPQ::sink(int k, int N)
{
while (2 * k <= N) {
int j = 2 * k;
if (j < N && less(j, j + 1))j++;
if (!less(k, j))break;
exchange(k, j);
k = j;
}
}
void HeapSort(int arr[], int n) {
MaxPQ m(arr, n);
int N = m.size();
while (N > 1) {
m.exchange(1, N--);
m.sink(1, N);
}
for (int i = 1;i <= m.size();i++)
{
arr[i - 1] = m.pq[i];
}
}