我们在第四章学习了使用数组实现的队列。这样的队列就是在tail的位置入列 (插入数据),在head的位置出列 (删除数据)。遵循“先进先出”的原则。这里我们要介绍的“优先级队列”是用“堆”实现的。“优先级队列”不再遵循“先进先出”的规则。小顶堆实现的优先级队列遵循“优先级最低的元素最先出列”的原则,大顶堆实现的优先级队列遵循“优先级最高的元素最先出列”的原则。
堆排序与优先级队列的区别:
下面的例子中,入列的元素大小是无序的,实现的是大顶堆。所以这个优先级队列是按照从大到小次序出列的。下面,我们就用代码实现优先级队列:
数组arrResource中存放着入列用的10个元素:
int arrResource[10] = {
3, 4, 1, 2, 3, 6, 5, 9, 3, 8}; //入列的元素
int priorQueue [10]; //用数组实现堆,用堆实现优先级队列
int totalNum = 10; //arrResource中一共有10个元素
int size = 0; //优先级队列中有0个元素
int dequeuedNum = 0; //已经出列的元素个数
与堆排序相比,这里一共多了四个函数:
1、显示优先级队列中的所有元素。这个函数就是简单的遍历数组。
void showQueue()
{
for(int i = 0; i < size; i++)
{
printf("%d, ", priorQueue[i]);
}
printf("\r\n");
}
2、优先级队列的入列。数据进入优先级队列之后可能会破坏堆,所以要重新维护堆。如果一共要入列k个元素,则现场维护堆的时间成本是O(K * logN),而完全重建堆的时间复杂度为O(N)。这时会有一个什么时候维护堆的问题。如果只是入列一两个元素,那就应该选择现场维护。但是如果一次性入列很多个元素,那么现场维护的时间成本O(K * logN) 可能高于重新建堆 O(N),所以应该选择完全重建堆。这里,我们选择现场维护的方式。
void enqueue(int n) //把元素n入列
{
priorQueue[size] = n; //入列
size++; //优先级队列中的元素 + 1
shiftUp(size - 1); //每次入列都进行维护。注意,这是向上调整
}
3、优先级队列的出列。利用堆的方式出列:只出列堆顶元素,且在出列 (堆中的元素发生变化) 之后重新维护堆,使剩余元素符合大顶堆的要求。这样才能保证每次输出的都是最大值。
int dequeue() //优先级队列出列,得到最大值
{
if(size <= 0) //防止出错
{
return -1; //返回-1表示出错
}
int max = priorQueue[0]; //记录当前堆中的最大值
//这三行就是堆排序中的一次循环
swap(0, size - 1); //把首元素与尾元素交换
size--; //堆的大小-1
shiftDown(0); //重新维护堆:向下调整数据
dequeuedNum++; //出列的元素个数++
return max; //返回最大值
}
4、优先级队列中的元素的修改。堆排序算法中不修改堆中的元素的数据。但是在优先级队列中可以修改。那么问题来了:对于一个大顶堆,如果修改之后的值比原先大,该怎么调整;如果修改之后的值比原先小,又该怎么调整呢?
把所有数据重建堆 (完整维护堆) 当然是一个可行的方法,但是这样做的时间复杂度为O(N)。由于绝大部分数据都在正确的位置,所以不如做一次部分维护。部分维护的时间复杂度为O(logN)。因为这是一个大顶堆,所以如果修改之后的数比原先大,那么这个数就应该尽量往堆顶方向调整,也就是调用shiftUp() 函数进行部分维护;相反,如果修改之后的数比原先小,就应该调用shiftDown() 函数。
//把堆中priorQueue[index]的值修改成num。修改后要重新维护堆,采用部分维护。
void editNum(int index, int num)
{
int num0 = pr