优先队列
一、优先队列的概念
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。
二、优先队列的API
优先队列是一种抽象数据结构,它表示了一组值和对这些值的操作,它的抽象层使我们能够方便地将应用程序和各种具体实现隔离开来。
优先队列最重要的操作就是删除最大元素和插入元素,删除最大元素的方法名为delMax()。插入元素的方法名为insert()。
为了保证灵活性,在实现中使用泛型,将实现了Comparable接口的数据的类型作为参数Key。这使得我们可以不必再区别元素和元素的键,对数据类型和算法的描述也将更加清晰和简洁。
- 泛型优先队列的API
public class MaxPQ <Key extends Comparable<Key>>{
// 创建一个优先队列
MaxPQ();
// 创建一个最大容量为max的优先队列
MaxPQ(max);
// 用a[]中的元素创建一个优先队列
MaxPQ(Key[] a);
// 向优先队列中插入一个元素
void Insert(Key v);
// 返回最大元素
Key max();
// 删除并返回最大元素
Key delMax();
// 返回队列是否为空
Boolean isEmpty();
// 返回优先队列中的元素个数
int size();
}
三、优先队列的初级实现
我们可以使用有序或无序的数组或链表。在队列较小时,大量使用两种主要操作之一时,或是所操作元素的顺序已知时,它们十分有用。因为实现相对简单。
(一)、数组实现(无序)
实现优先队列的一种简单方法就是基于前文中初级排序算法的文章的下压栈的代码。insert()方法的代码和栈的push()方法完全一样。要实现删除最大元素,可以通过添加一段类似于选择排序的内循环的代码,将最大元素和边界元素交换然后删除它,和对栈的pop()方法的实现一样。同栈类型,可以通过加入调整数组大小的代码来保证数据结构中至少含有四分之一的元素而又永远不会溢出。
(二)、数组实现(有序)
另一种方法就是在insert()方法种添加代码,将所有较大的元素向右边移动一格以使数组保持有序(和插入排序一样)。这样最大的元素总会在数组的一遍,有序队列的删除最大元素操作就和栈的pop()操作一样了。
(三)、链表表示法
同刚刚类似,可以基于链表的下压栈的代码作为基础,而后可以选择修改pop()来找到并返回最大元素,或是修改push()来保证所有元素为逆序并用pop()来返回来年表的首元素(即最大的元素)。
使用无序序列是解决这个问题的惰性方法,我们仅在必要的时候才会采取行动(找出最大元素);使用有序序列则是解决问题的积极方法,因为我们会尽可能未雨绸缪(在插入元素时就保持列表有序),使后续操作更高效。
实现栈或是队列与实现优先队列的最大不同在于对性能的要求。对于栈和队列,我们的实现能够在常数时间内完成所有操作。而对于优先队列,刚刚讨论过的所有初级实现种,插入元素和删除最大元素这两个操作之一在最坏情况下需要线性时间来完成。
(四)、图表分析
1、优先队列的各种实现在最坏情况下运行时间的增长数量级
数据结构 | 插入元素 | 删除最大元素 |
---|---|---|
有序数组 | N | 1 |
无序数组 | 1 | N |
堆 | logN | logN |
理想情况 | 1 | 1 |
2、在一个优先队列上只需的一系列操作
操作 | 参数 | 返回值 | 大小 | 内容(无序) | 内容(有序) |
---|---|---|---|---|---|
插入元素 | P | 1 | P | P | |
插入元素 | Q | 2 | P Q | P Q | |
插入元素 | E | 3 | P Q E | E P Q | |
删除最大元素 | Q | 2 | P E | E P | |
插入元素 | X | 3 | P E X | E P X | |
插入元素 | A | 4 | P E X A | A E P X | |
插入元素 | M | 5 | P E X A M | A E M P X | |
删除最大元素 | X | 4 | P E M A | A E M P | |
插入元素 | P | 5 | P E M A P | A E M P P | |
插入元素 | L | 6 | P E M A P L | A E L M P P | |
插入元素 | E | 7 | P E M A P L E | A E E L M P P | |
删除最大元素 | P | 6 | E E M A P L | A E E L M P |
四、堆的定义
(一)、堆的概念
数据结构二叉堆能够很好地实现优先队列的基本操作。在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素。相应地,这些位置的元素又至少要大于等于数组中的另两个元素,以此类推。
- 当一颗二叉树的每个结点都大于等于它的两个子结点时,它被称为堆有序。
相应地,在堆有序的二叉树中,每个结点都小于等于它的父节点(如果有的话)。从任意结点向上,我们都能得到一列非递减的元素;从任意节点向下,我们都能得到一列非递减的元素。
- 根节点是堆有序的二叉树的最大结点。
(二)、二叉堆表示法
如果使用指针来表示堆有序的二叉树,那么每个元素都需要3个指针来找到它的上下结点(父节点和两个子结点各需要一个)。
如果我们使用完全二叉树,表达就会变得特别方便。完全二叉树只用数组而不需要指针就可以表示,具体方法就算将二叉树的结点按照层级顺序放入数组中,根节点在位置1,它的子结点在位置2和3,而子结点的子结点分别在位置4,5,6和7,以此类推。
- 二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不使用数组的第一个位置)
使用数组实现的完全二叉树的结构是很严格的,但它的灵活性足以让我们高效地实现优先队列。用它们我们将能实现对数级别的插入元素和删除最大元素的操作。利用在数组中无序指针即可沿树上下移动的遍历,算法保证了对数复杂度的性能。
- 一棵大小为N的完全二叉树的搞得为(lgN)的向下取整
五、堆的算法
优先队列由一个基于堆的完全二叉树表示,存储与数组pq[1…n]中,pq[0]没有使用。在insert()方法中,将n加1并把新元素添加在数组最后,然后使用swim()方法恢复堆的秩序。在delMax()中,从pq[1]中得到需要返回的元素,然后将pq[n]移动到pq[1],将n减1并使用sink()恢复堆的秩序,同时将不再使用的pq[n+1]设为null。
- 对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大元素的操作需要不超过2lgN次比较。
(一)、堆实现的比较和交换方法
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j) {
Key t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
(二)、由下至上的堆有序化(上浮)
private void swim(int k) {
while (k > 1 && less(k / 2, k)) {
exch(k / 2, k);
k = k / 2;
}
}
(三)、由上至下的堆有序化(下沉)
private void sink(int k) {
while (2 * k <= n) {
int j = 2 * k;
if (j < n && less(j, j + 1)) {
j++;
}
if (!less(k, j)) {
break;
}
exch(k, j);
k = j;
}
}
(四)、堆的类定义以及相关方法
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq;
private int n = 0;
public MaxPQ(int maxN) {
pq = (Key[]) new Comparable[maxN + 1];
}
public boolean isEmpty() {
return n == 0;
}
public int size() {
return n;
}
public void insert(Key v) {
pq[++n] = v;
swim(n);
}
public Key delMax() {
// 从根节点得到最大元素
Key max = pq[1];
// 将其和最后一个记得交换
exch(1, n--);
// 防止越界
pq[n + 1] = null;
// 恢复堆的有序性
sink(1);
return max;
}
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
private void exch(int i, int j) {
Key t = pq[i];
pq[i] = pq[j];
pq[j] = t;
}
private void swim(int k) {
while (k > 1 && less(k / 2, k)) {
exch(k / 2, k);
k = k / 2;
}
}
private void sink(int k) {
while (2 * k <= n) {
int j = 2 * k;
if (j < n && less(j, j + 1)) {
j++;
}
if (!less(k, j)) {
break;
}
exch(k, j);
k = j;
}
}
}