优先队列(PriorityQueue)与普通的队列(Queue)不同,不遵循“先进先出”的原则,而是遵循“最小元素先出”的原则。由于历史原因,人们已习惯于用“队列”为后缀称呼这种结构,因此就沿用了优先队列这个名字。优先队列有三个基本操作:插入一个元素,找出最小的元素,删除最小的元素。
优先队列如何实现?
可以用单链表实现,这样插入一个元素的时间代价为常数,删除最小元素需要搜索整个链表,时间代价为O(n);也可以用排序的单链表实现,可以实现对最小元素的快速查找和删除,但是插入元素时,时间代价却是O(n)。
最常用的实现方式是借用堆,堆的定义如下,
1)小根堆:等价于所有根都小于等于其左、右子节点的完全二叉树。
2)大根堆:等价于所有根都大于等于其左、右子节点的完全二叉树。
满足这一特征称之为堆序性。下面以小根堆为例讨论优先队列的实现,
由于堆和二叉树的内在联系,优先队列的定义与二叉树的顺序表示一样:
typedef struct PriorityQueue* ppq; //指向优先队列的指针类型
struct PriorityQueue{
int MAXN; //堆中元素个数的上限,
int n; //堆中实际元素个数
DataType* pq; //优先队列类型
};
插入操作
向优先对接加入一个新的元素,即在堆里增加一个元素,根据堆的顺序表示,为了保持堆序性,先把新元素放在最后位置,然后通过反复比较,必要时交换该结点和对应的父节点,直到重新满足堆序性(即直到新节点升到了某一位置,发现它的父节点比它小或者它已经是根)为止。例如,对于上图的堆中插入元素4,
首先将4插入16的后面,然后将4的父节点16与4比较,16大于4则将16移到4的位置,此时原来16的位置“空”出来;
然后将上面的“空”位置的父节点10与4比较,10大于4则将10移到“空”位置,此时原来的10的位置“空”出;
然后将上面的“空”位置的父节点3与4比较,发现3小于4则将4插入到这个“空”位置。
这样,保持了堆序性,这种方法称作筛选法。插入的算法如下:
void addHeap(ppq p, DataType x)
{
int i;
if(p->n >= p->MAXN){
cout<<"Full!\n"; return;
}
for(i = p->n; i>0 && p->pq[(i-1)/2]>2; i=(i-1)/2){
p->pq[i] = p->pq[(i-1)/2];
}
p->pq[i] = x; p->n ++;
}
因为在循环里每次都把“空”位上移一层,最多可上移㏒2n次,所以用堆实现的优先队列的插入操作的时间代价为O(㏒2n)。
删除操作
删除操作的方法和插入时的类似,但筛选的方向相反。在最小结点被删除后,根结点形成一个空位,这时考虑能否把堆中最后位置的元素的结点填入这里。为了保持堆序性,可以在最后的元素和空位的两个结点三者中,选择最小者填入,选择的结果可能使得原来的空位向叶节点方向“移动”。如此反复交换,直到堆中最后结点小于等于空位的两个子节点时,将最后结点填入这个空位。
例如有一个最小堆,
首先删除最小元素2,空出位置然后用最后的元素11和空位的两个子节点3和4作比较,发现3最小则将3提升到空位,原来3的位置空出;
继续将11和当前空位的两个子节点9和10作比较,9最小则将其提升到空位,原来9的位置空出;
再继续将11和当前空位的两个子节点12和12作比较,11最小则将其填入空位,删除过程结束。
此时仍保持堆序性,删除结果如下,
删除的算法如下,
void removeMinHeap(ppq p)
{
if(p->n == 0){ //堆为空
cout<<"Empty!\n"; return;
}
int k = p->n - 1; //删除根结点,k值为最后元素的下标
DataType temp = p->pq[k]; //存放最后的元素
int emptyIndex = 0; //空位结点的下标,此时为根结点的
int index = 1; //因为已删除根结点,index从1开始
while(index < k){ //找最后结点存放的位置
if(index < k-1 && p->pq[index] > p->pq[index + 1])
index++; //选择比较小的结点
if(temp > p->pq[index]){ //最后结点最大,则空位向叶节点移动
p->pq[emptyIndex]=p->pq[index];
emptyIndex = index;
index = index*2 +1;
}
else break; //已经找到合适的位置
}
p->pq[emptyIndex] = temp; //把最后的元素填入空位
}
删除过程中,由于对二叉树的每层最多只需要做2次比较,而且循环是从树根到树叶进行的,所以用堆实现的优先队列的删除操作的时间代价也是O(㏒2n)。
优先队列的应用十分广泛,在操作系统中的进程调度和对于现实世界各种离散时间的模拟过程中,它都是十分有效的工具。另外,利用堆进行的排序称为堆排序,详解见 http://blog.csdn.net/gnosed/article/details/78768679 。