首先说明一点,不要被名字所误导,优先队列并不是一种排序饿算法,而是一种数据结构。这个结构与一般的先入先出的队列不同,它每次出队的是优先级最大的元素。当一个指定优先级的元素入队时,能够很快的把它排到队列中。把这两个放在一起,是因为优先队列是用堆排序的方法设计的。
首先先看堆排序吧。堆是一个完全二叉树。完全二叉树我们之前已提过,就是它的前面是满的,直到最后一层除外。最后一层从左边起一直都有叶子,但是从某一个节点开始,这个节点的后面全都没有叶子了。(用嘴说这个数据结构真费心,如果我没说清楚,大家自行百度吧!)这个结构有一个明显的好处在于,他可以方便的使用数组(而不是指针)来实现,如果对整个完全二叉树从左向右,从上大小,从0开始编号的话,数组的下标i对应着某一个节点,那么这个节点的左右孩子分别为:2*i+1、2*i+2;它的父节点为(i-2)/2。
讲完了完全二叉树,我们再看看最大堆与最小堆。最大堆是这样一种结构:它的孩子节点的key都小于等于父节点;如果孩子节点的key都大于等于父节点,就称为最小堆。
有了这个写概念,我们就可以讲堆排序了。
堆排序的过程大致分为4大步:1.利用原来的数组建立一个最大堆。此时,数组中最大的元素肯定是在data[0]中保存的。2.把最大堆的data[0]与data[n-1]元素交换,此时最大的元素就跑到data[n-1]中了。3.忽略最后一个元素,调整整个堆,是得元素交换以后又是一个最大堆。4.将最大堆的data[0]与data[n-2]进行交换,以此类推。
整个程序的难点在于:如何调整一个堆,让他变为最大堆。这个问题可以递归的解决:假设某个节点的左右子树已经是最大堆了,那么如需要将根节点与左右子树相比较,然后(如果需要的话)与左右子树中最大的交换;但是交换完以后,可能会引起左右子树中的一颗不再是最大推,那么就对于以左(或者右)子树为根的子树继续重复前面的过程,直到遇见叶子节点。这个部分的程序如下:
//调整堆,使以index为根的子树成为最大堆
void heapAdjust(pArrayList list, int index,int length)
{
//左孩子
int lchild = index * 2 + 1;
//右孩子
int rchild = index * 2 + 2;
//最大的下标
int largest ;
//找出最大值的下标,存放在largest中
if(lchild <= length && list->data[lchild] > list->data[index])
largest = lchild;
else
largest = index;
if(rchild <= length && list->data[rchild] > list->data[largest])
largest = rchild;
//如果需要交换
if(largest != index)
{
//交换它们
int tmp = list->data[index];
list->data[index] = list->data[largest];
list->data[largest] = tmp;
//当交换以后,需要判断下一级的3个节点是否需要调整
heapAdjust(list,largest,length);
}
}
有了这个函数,其他问题就好办多了:比如利用数组初始化一个最大堆,就需要从最后一个叶子节点的父节点开始,依次调用上面的heapAdjust函数就行了:
//自底而上的调用heapAdjust将数组变成一个最大堆
void BuildMaxHeap(pArrayList list)
{
//从最后一个父亲节点开始,直到树根
for(int i = (list->length-2)/2; i >= 0; --i)
heapAdjust(list,i,list->length);
}
剩下的内容就如前面所述:将最大堆的跟与数组的最后一个元素交换,然后忽略最后一个元素,重新调整堆。然后取倒数第二个元素重复上述步骤:
//堆排序
void heapSort(pArrayList list)
{
BuildMaxHeap(list);
printf("构成最大堆:");
printArrayList(list);
int len = list->length-1;
for(int i = list->length-1; i > 0; --i)
{
int tmp = list->data[0];
list->data[0] = list->data[i];
list->data[i] = tmp;
printf("交换以后");
printArrayList(list);
//排好以后就该忽略最后一个元素,然后考虑前n-1个元素了
--len;
heapAdjust(list,0, len);
}
printf("最终结果:");
printArrayList(list);
}
个人觉得,这个算法虽然设计精巧,但是与前面讲到的快速排序和归并排序相比,运算量着实不小。它的一个重要的应用在于:实现优先队列。
让我们简单的思考一下思路:对于一个优先队列,需要支持如下几种关键的操作:(其他什么判断队列是否为空,返回队列长度之类的就省略了)
(1)push:将元素插入优先队列。
(2)top:返回优先级最高的元素
(3)pop:删除优先级最高的元素
(4)increase:将某个元素的优先级增加到某个数
有了前面的堆排序,我们就不会对这些操作一脸茫然了,当我们已经有一个最大堆时:
(2)操作:就是返回一个最大推的第一个元素A[0];
(3)操作:与堆排序类似,把A[0]用A[n-1]代替,然后将数组的长度减1(忽略最后一个元素),然后调整堆就可以了。
(4)操作:将A[i]的设为指定的优先级。然后在i>1且i的父节点一直小于i的情况下,一直做两件事:a.交换A[i]与它的父节点的值。b.将i递增为父节点的序号。
(1)操作:有了(4)操作,其实(1)就很容易实现了,给堆中插入一个优先级非常低的节点,然后通过increase函数将它的优先级提升的指定值就可以了。
下面看一下具体的函数:
//返回队首元素
int top(pArrayList list)
{
return list->data[0];
}
//出队
int pop(pArrayList list)
{
int head = list->data[0];
list->data[0] = list->data[list->length-1];
--list->length;
//调整堆为最大堆
heapAdjust(list,0,list->length);
return head;
}
//将指定位置的优先级升高为一个数
void increaseKey(pArrayList list, int index, int key)
{
if(list->data[index] > key)
{
printf("new key must bigger than old key!\n");
return ;
}
else
list->data[index] = key;
int tmp;
//当子节点不为树根且父节点小于子节点时
while(index > 0 && list->data[(index-2)/2] < list->data[index])
{
//交换父节点与子节点的元素
tmp = list->data[index];
list->data[index] = list->data[(index-2)/2];
list->data[(index-2)/2] = tmp;
//继续向上一层走
index = ( index - 2 ) / 2;
}
}
//入队
void push(pArrayList list, int key)
{
if(list->length == list->size)
{
list->data = (int*)realloc(list->data,sizeof(int) * list->size * 2);
list->size = list->size*2;
}
//初始优先级设为最小
list->data[list->length] = -1;
increaseKey(list,list->length,key);
++list->length;
}