普通队列就是遵循时间原则,先进先出,后进后出的一种队列结构;而优先队列则是按照优先级来决定出队的次序,与何时进入队列无关,实际生活中有很多优先队列的例子:医院候诊病人中,急诊病人优先看病,而不是按照病人到达医院的先后次序来,这就是一个优先队列的例子。
优先队列适合应用于动态场景中,比如游戏中自动作战,当存在多个敌人时,不是按照敌人出现的顺序进行攻击,而是根据某个优先级(比如先攻击血量最大的)进行攻击,为什么说是动态的?是因为不是在攻击完血量最大的敌人之后就接着攻击剩余的敌人,实际上还会有其他的敌人陆续出现,这时候再在所有敌人中选出优先级最高的进行攻击。
实际上,除了在动态数据的处理上优先队列很有优势,优先队列在处理静态数据时有时也是很有优势的。
优先队列的实现方式有几种:
当总共需要进行N次时,采用普通数组和顺序数组实现的优先队列的时间复杂度最差为O(n^2),而采用堆实现的优先队列时间复杂度为O(Nlogn)。
接下来介绍堆这种数据结构。
什么是堆?经典的是二叉堆,对应的就是一棵二叉树,二叉堆又分为最大堆和最小堆。最大堆:堆中每个节点的值都不大于其双亲节点的值,根结点为最大值;最小堆:堆中每个节点的值都不小于其双亲节点的值,根结点为最小值。二叉堆总是一棵完全二叉树。
二叉堆的经典存储方式是使用数组来存储。
根据这种存储方式,对数组中下标为 i 的元素,其双亲结点就对应数组中下标为 i/2 的元素,其左孩子为数组中下标为 2i 的元素,其右孩子为数组中下标为 2i+1 的元素。
接下来以最大堆为例,用C++实现最大堆:
#include <iostream>
#include <cassert>
#include <algorithm>
#include <cstdlib>
#include <ctime>
using namespace std;
template<typename Item>
class MaxHeap
{
private:
Item *data;
int count;
int capacity;
void shiftUp(int k)
{
//凡是涉及到数组下标的,都要限定它不出界
while(k > 1 && data[k] > data[k / 2])
{
swap(data[k], data[k / 2]);
k /= 2;
}
}
void shiftDown(int k)
{
//当k有孩子的情况下开始shiftDown操作
while( 2*k <= count )
{
int j = 2*k; //j就是要和data[k]交换的元素的位置下标
//判断是否有右孩子
if( j+1 <= count && data[j+1] > data[j] )
{
j = j + 1;
}
if(data[k] >= data[j])
{
break;
}
swap(data[k], data[j]);
k = j;
}
}
public:
MaxHeap(int capacity)
{
data = new Item[capacity + 1]; //从1号单元开始存放
count = 0;
this->capacity = capacity;
}
~MaxHeap()
{
delete[] data;
}
//返回堆现在的大小
int size() const
{
return count;
}
//判断堆是否为空堆
bool isEmpty() const
{
return count == 0;
}
//向堆中插入一个元素
void insert(Item item)
{
//判断count+1是否出界
assert(count + 1 <= capacity);
data[count + 1] = item;
count++;
//从count位置处开始向上调整堆
shiftUp(count); //这个函数不需要被用户调用,所以设为私有
}
//取出堆中的一个元素(只能取出根结点)
Item extractMax()
{
//首先判断堆是否为空
assert(count > 0);
Item ret = data[1];
data[1] = data[count];
count--;
shiftDown(1);
return ret;
}
//打印堆中内容
void printData() const
{
for(int i = 1; i <= count; i++)
{
cout << data[i] << " ";
}
cout << endl;
}
};
int main()
{
MaxHeap<int> maxheap = MaxHeap<int>(100); //利用构造函数初始化
cout << maxheap.size() << endl;
//随机插入几个元素
srand(time(0));
while(maxheap.size() < 10)
{
int item = rand() % 100 + 1;
maxheap.insert(item);
}
cout << maxheap.size() << endl;
maxheap.printData();
//取出堆中元素(实际上就是将堆中元素从大到小排序的过程)
while(!maxheap.isEmpty())
{
cout << maxheap.extractMax() << " ";
}
cout << endl;
return 0;
}
我们将最大堆声明为一个类,因为堆的大小是由用户定义的,所以使用指针 Item *data 来动态分配内存(使用构造函数 MaxHeap(int capacity)来实现),在构造函数中使用 new[] 动态分配内存后,相应的就需要在析构函数中 delete[] 内存。
接下来分析向堆中插入元素的过程:只能在数组的最后一个位置插入元素,此时的完全二叉树不满足堆的定义,所以需要进行向上调整堆的过程,若新插入节点值大于其双亲节点的值,则交换两者位置,重复此步骤直至根结点(由于用户只需要调用插入元素的函数,不需要调用向上调整的函数,所以将shiftUp(int k)定义在私有成员中)。
接下来分析取出堆中元素的过程:只能取出根节点的值(最大值),此时根节点的值用堆中最后一个元素的值代替,此时新的完全二叉树不满足堆的定义,所以需要向下调整堆。若根节点的值小于其左右孩子中的较大值,则交换两者位置,重复此步骤,直到其没有孩子为止(由于用户只需要调用取出元素的函数,不需要调用向下调整的函数,所以将shiftDown(int k)定义在私有成员中)。
在main函数中,最后的while(!maxheap.isEmpty())循环按从大到小的顺序打印出堆中所有元素,实际上就是一个排序的过程,打印结果如下:
在上述代码中,需要多次进行swap操作,这是一项耗时的操作,最好使用赋值操作来取代之:即先找到要交换的位置,然后赋值。优化后的 shiftUp() 和 shiftDown() 代码如下:
void shiftUp(int k)
{
//凡是涉及到数组下标的,都要限定它不出界
//优化:用赋值取代交换
Item item = data[k];
while( k>1 && data[k/2] < item )
{
data[k] = data[k/2];
k /= 2;
}
data[k] = item;
}
void shiftDown(int k)
{
//当k有孩子的情况下开始shiftDown操作
//优化:用赋值取代交换
Item item = data[k];
while( 2*k <= count )
{
int j = 2*k;
if(j + 1 <= count && data[j + 1] > data[j])
{
j = j + 1;
}
if(data[j] <= item)
{
break;
}
data[k] = data[j];
k = j;
}
data[k] = item;
}
到这里,如何实现最大堆就讲完了,下一节将实现堆排序以及其优化。