在实际环境中,普通的FIFO的队列不能保证整体效率达到最高,如CPU的多任务调度,医院诊疗等,简单的“先入先出”原则并不是很好的方案,对大多数问题而言,应该根据事物特性的指定合适的优先级方案灵活的操作,而优先级队列很适合处理这样的问题。
一、优先级队列
此前的搜索树结构和词典结构,都支持覆盖数据全集的访问和操作,它们定义并维护显式或者隐式的全序关系(full order)。就对外接口的功能而言,优先级队列,较之此前的数据结构反而有所削弱。
优先级队列将操作对象限定于当前的全局极值者。比如,在全体北京市民中,查找年龄最长者,或者在所有鸟类中,查找种群规模最小者,等等。这种根据数据对象之间相对优先级对其进行访问的方式,与此前的访问方式有着本质区别,称循优先级访问(call-by-priority)。优先级队列并不动态维护整个容器内元素的全序,却转而维护一个偏序(partial order) ,其高明之处在于,如此不仅足以高效地支持仅针对极值对象的接口操作,更可有效地控制整体计算成本。
优先级队列:按照事先约定的优先级,可以始终高效查找并访问优先级最高数据项的数据结构,统称为优先级队列(priority queue)
二、堆
基于列表或向量等结构的实现方式,之所以无法同时保证insert()和delMax()操作的高效率,原因在于其始终都保存了全体词条之间的全序关系。实际上,尽管优先级队列的确隐含了“所有词条可相互比较”这一条件,但从操作接口层面来看,并不需要真正地维护全序关系,只要能够确定全局优先级最高的词条即可。
事实上,此前的向量、列表等数据结构,都不能同时保证插入、删除、取优先级最大的元素这三个接口的时间复杂度均在O(logn)范围内,虽然平衡二叉树中的AVL树、伸展树、红黑树可以做到,但是他们的功能对于实现优先级队列来说功能过于强大,可谓是杀鸡用牛刀,而利用堆实现的优先级队列则是一种轻量级数据结构,从实现成本上来看更加适合!
三、完全二叉堆
堆有多种实现形式,其中最基本的一种形式即为完全二叉堆(complete binary heap)。完全二叉堆应满足两个条件:
(a) 结构性:其逻辑结构必须等价于完全二叉树,所以对于有n个节点的完全二叉堆,其高度h=O(logn);
(b) 堆序性:就优先级而言,堆顶以外的每个节点都不高(低)于其父节点,分别称为大(小)堆。
完全二叉堆的拓扑联接结构,完全由其规模n确定,按照层次通历的次序,每个节点都对应于唯一的编号,反之亦然,故若将所有节点组织为一个向量,则堆中各节点(编号)与向量各单元(秩)也将彼此一对应。事实上,完全二叉堆实际就是一个vector,只不过用二叉树的概念来操作,节点之间的父子关系是通过秩的计算公式来描述的。
父子关系计算:设i(x)表示节点x所对应的秩,则对于完全二叉堆中的任意节点v,必然满足
(a) 若v有左孩子,则其左孩子对应的秩 i(lchild(v))=2*i(v)+1;
(b) 若v有右孩子,则其右孩子对应的秩 i(rchild(v))=2*i(v)+2;
(c) 若v有父节点,则其父节点对应的秩 i(parent(v))=[i(v)/2](上取整)-1;
四、完全二叉堆的操作
(1) 元素插入
(a) 首先调用向量的标准插入接口,将新词条接至向量的未尾,这个步骤不会导致完全二叉堆的结构性发生变化。
(b) 只要新插入的词条e不是堆顶,就有可能与其父亲违反堆序性,需要从e开始进行上滤操作进行堆序性的调整。
上滤操作:节点e与其父亲进行关键码(优先级的比较),若违背堆序性,则令二者交换位置。当然,交换之后可能导致上层祖先节点之间的堆序性异常,故需要继续往上迭代,知道不再发生堆序性异常或者到达根节点 。
性能:上滤过程的总交换次数不会超过全堆的总高度,而每次交换需要常数时间,所以上滤操作及整个插入操作的时间复杂度为O(logn)。
(2) 元素删除
(a) 首先,既然待删除词条r总是位于堆顶,故可直接将其取出并备份,为防止堆的结构性将被破坏,将堆尾的节点转移到堆定。
(b) 新节点可能与其孩子违背堆序性,为了修复这一缺陷,需要从堆顶开始进行下滤操作进行调整。
下滤操作:若堆顶e不满足堆序性,可将e及其孩子中的最大者作为新的堆顶,此后可能被交换的孩子可能导致再次违背堆序性,则需要继续进行下滤迭代,直到没有堆序性异常或者到达叶节点。
性能:下滤过程的总交换次数不会超过全堆的总高度,而每次交换需要常数时间,所以下滤操作及整个删除操作的时间复杂度为O(logn)。
(3) 建堆(Floyd算法)
多数情况下,输入词条集均以向量的形式给出,故除了通过个单元的秩明确对应的父子关系外,不需要做其他实质的操作。因为二叉堆本身就是一个向量,所以其结构性已经得到了满足,但是完全二叉堆的堆序性却很可能不满足,所以需要从堆的最后一个内部节点开始进行下滤操作,一直到第一个向量元素即可,这即为Floyd算法---自下而上的下滤。
性能:算法需要做n次下滤,每个节点下滤的所需要的实际正比于其高度,所有总体运行时间取决于各节点高度的总和,经过计算为O(n)。
五、完全二叉堆的实现
首先建立了优先级队列的模板类PQ,描述了优先级队列的通用操作接口,然后基于vector和PQ的多继承得到完全二叉树类PQ_ComplHeap,其具体实现了完全二叉树的插入和删除操作,以及上滤和下滤操作。
操作 | 功能 | 对象 |
insert(T) | 按照比较器确定的优先级次序插入词条 | 优先级队列 |
getMax() | 取出优先级最高的词条 | 优先级队列 |
delMax() | 删除优先级最高的词条 | 优先级队列 |
操作 | 功能 | 对象 |
PQ_ComplHeap() | 默认构造函数 | 完全二叉堆 |
PQ_ComplHeap(vector<T> &A, Rank n) | 构造函数,由向量批量构造 | 完全二叉堆 |
percolateDown(Rank n, Rank i) | 下滤 | 完全二叉堆 |
percolateUp(Rank i) | 上滤 | 完全二叉堆 |
heapify(Rank n) | Floyd建堆算法 | |
insert(T e) | 按照比较器确定的优先次序,插入词条 | 完全二叉堆 |
getMax() | 读取优先级最高的词条 | 完全二叉堆 |
delMax() | 删除优先级最高的词条 | 完全二叉堆 |
(1) PQ.h
#pragma once
template<typename T> struct PQ //优先级队列PQ模板类
{
virtual void insert(T) = 0; //按照比较器确定的优先级次序插入词条
virtual T getMax() = 0; //取出优先级最高的词条
virtual T delMax() = 0; //删除优先级最高的词条
};
(2) PQ_ComplHeap.h
#pragma once
#include"vector.h"
#include"PQ.h"
#define lt(a,b) ((a)<(b)) //比较大小
#define InHeap(n,p) (((-1)<(i))&&((i)<(n))) //判断PQ[i]是否合法
#define Parent(i) ((i-1)>>1) //PQ[i]的父节点(floor((i-1)/2),i无论正负)
#define LastInternal(n) Parent(n-1) //最后一个内部节点(末节点的父亲)
#define LChild(i) (1+((i)<<1)) //PQ[i]的左孩子
#define RChild(i) ((1+(i))<<1) //PQ[i]的右孩子
#define ParentValid(i) (0<(i)) //判断PQ[i]是否有父亲
#define LChildValid(n,i) InHeap(n,LChild(i)) //判断PQ[i]是否有一个(左)孩子
#define RChildValid(n,i) InHeap(n,RChild(i)) //判断PQ[i]是否有两个孩子
#define Bigger(PQ,i,j) (lt(PQ[i],PQ[j])?j:i) //取大者
#define ProperParent(PQ,n,i) \
(RChildValid(n,i)?Bigger(PQ,Bigger(PQ,i,LChild(i)),RChild(i)):\
(LChildValid(n,i)?Bigger(PQ,i,LChild(i)):i\
)\
)//父子三者中的最大者
template<typename T> class PQ_ComplHeap :public PQ<T>, public vector<T>
{
protected:
Rank percolateDown(Rank n, Rank i); //下滤
Rank percolateUp(Rank i); //上滤
void heapify(Rank n); //Floyd建堆算法
public:
//构造函数
PQ_ComplHeap() {}
PQ_ComplHeap(vector<T> &A, Rank n); //批量构造
void insert(T e); //按照比较器确定的优先次序,插入词条
T getMax(); //读取优先级最高的词条
T delMax(); //删除优先级最高的词条
};
template<typename T> PQ_ComplHeap<T>::PQ_ComplHeap(vector<T> &A, Rank n)
{
copyFrom(A, 0, n);
heapify(n);
}
template<typename T> T PQ_ComplHeap<T>::getMax()
{
return _elem[0]; //返回向量的首元素
}
template<typename T> void PQ_ComplHeap<T>::insert(T e)
{
vector<T>::insert(e); //首先将新词条接至向量末尾
percolateUp(_size - 1); //再对该词条实施上滤调整
}
template<typename T> Rank PQ_ComplHeap<T>::percolateUp(Rank i)
{
while (ParentValid(i)) //只要顶点还有父亲
{
Rank j = Parent(i); //将i之父记作j
if (lt(_elem[i], _elem[j])) break; //一旦当前父子不在逆序,则上滤完成
swap(_elem[i], _elem[j]);
i = j; //继续向上
}
return i; //返回上滤最终抵达的位置
}
template<typename T> T PQ_ComplHeap<T>::delMax()
{
T maxElem = _elem[0]; //缓存待删除的堆顶
_elem[0] = _elem[--_size]; //将末尾词条放至堆顶
percolateDown(_size, 0); //对新堆顶进行下滤
return maxElem; //返回删除的原堆顶
}
template<typename T> Rank PQ_ComplHeap<T>::percolateDown(Rank n, Rank i)
{
Rank j; //i节点及其孩子中,最大的词条对应的秩
while (i != (j = ProperParent(_elem, n, i))) //只要最大者不为i本身
{
swap(_elem[i], _elem[j]); //最大者变为新父亲
i = j; //继续向下
}
return i; //返回下滤最终到达的位置
}
template<typename T> void PQ_ComplHeap<T>::heapify(Rank n)
{
for (int i = LastInternal(n); InHeap(n, i); i--) //从堆末词条的父亲开始下滤
percolateDown(n, i); //下滤各内节点
}