介绍
优先队列是一种抽象数据类型,它表示了一组值和对这些值的操作,优先队列最重要的操作就是删除最大元素和插入元素。其API如下:
初级实现
数组实现(无序)
类似栈的数组实现。inser()方法的代码和栈的push()方法完全一样。要实现删除最大元素,我们可以添加一段类似于选择排序的内循环的代码,将最大元素和边界元素交换然后删除它。和栈类似,我们也可以加入调整数组大小的代码来保证数据结构中至少含有四分之一的元素而又永远不会溢出。
数组实现(无序)
另一种方法就是在insert()方法中添加代码,将所有较大的元素向右边移动一格以使数组保持有序(和插入排序一样)。这样,最大的元素总会在数组的一边,优先队列的删除最大元素操作就和栈的pop()操作一样了。
链表表示法
和刚才类似,我们可以用基于链表的栈的代码作为基础,而后可以选择修改pop()来找到并返回最大元素,或是修改push()来保证所有元素为逆序并用pop()来删除并返回链表的首元素(也就是最大的元素)。
总结
对于栈和队列,我们的实现能够在常数时间内完成所有的操作;而对于优先队列,我们刚刚讨论过的所有初级实现中,插入元素和删除最大元素这两个操作之一在最坏情况下需要线性时间来完成。
堆的实现
数据结构二叉堆能够很好地实现优先队列的基本操作。如果我们用指针来表示堆有序二叉树,那么每个元素都需要三个指针来找到它的上下结点(父结点和两个子结点各需要一个)。如果我们使用完全二叉树,表达就会变得特别方便。具体方法是将二叉树的结点按照层级顺序放入数组中。根结点在位置1,它的子结点在位置2和3,而子结点的子结点则分别在位置4、5、6和7,以此类推。用数组(堆)实现的完全二叉树的结构是很严格的,但它的灵活性已经足以让我们高效地实现优先队列。用它们我们将实现对数级别的插入元素和删除最大元素的操作。
命题P:一棵大小为N的完全二叉树的高度为
我们用长度N+1的数组pq表示一个大小为N的堆,我们不会使用pq[0],堆元素放在pq[1]至pq[N]中。堆的操作首先会进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态修复。我们称这个过程叫做堆的有序化。
由下至上的堆有序化(上浮)
如果堆的有序状态因为某个结点变得比它的父结点更大而被打破,那么我们就需要通过交换它和它的父结点来修复堆。交换后,这个结点比它的两个子结点都大。但这个结点仍然可能比它现在的父结点更大。我们可以一遍遍地用同样的方法恢复秩序,直到我们遇到一个更大的父结点。只要记住位置k的结点的父结点的位置是。
由上至下的堆有序化(下沉)
如果堆的有序状态因为某个结点变得比它的两个子结点或是其中之一更小而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆。类似的上浮,这个过程需要一遍遍重复,将结点向下移动直到它的子结点都比它更小或是到达了堆的底部。
插入元素
我们将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置。
删除最大元素
我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆的大小并让这个元素下沉到合适的位置。
实现代码
package Sort;
import java.util.NoSuchElementException;
import java.util.Scanner;
public class MaxPQ<Key extends Comparable<Key>> {
private Key[] pq;
private int N;
private static final int DEFAULT_CAPACITY = 2;
MaxPQ(){
this(DEFAULT_CAPACITY);
}
MaxPQ(int capacity){
pq = (Key[]) new Comparable[capacity + 1];
}
MaxPQ(Key[] arr){
this(arr.length);
N = arr.length;
System.arraycopy(arr, 0, pq, 1, N);
for(int k = N / 2; k >= 1; k--){
sink(k);
}
}
private void resize(int N){
Key[] newPq = (Key[]) new Comparable[N + 1];
System.arraycopy(pq, 1, newPq, 1, this.N);
pq = newPq;
}
private void swap(int i, int j){
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
private boolean less(int i, int j){
return pq[i].compareTo(pq[j]) < 0;
}
private void swin(int k){
while (k > 1 && less(k / 2, k)){
swap(k, k / 2);
k /= 2;
}
}
private void sink(int k){
while (k * 2 <= N){
int j = k * 2;
if(j < N && less(j, j + 1)) j++;
if(!less(k, j)) break;
swap(k, j);
k = j;
}
}
public void insert(Key v){
if (N == pq.length - 1) resize(2 * pq.length);
pq[++N] = v;
swin(N);
}
public Key max(){
if (isEmpty()) throw new NoSuchElementException("Priority queue underflow");
return pq[1];
}
public Key delMax(){
if (isEmpty()) throw new NoSuchElementException("Priority queue underflow");
Key key = max();
swap(1, N);
pq[N--] = null;
sink(1);
if ((N > 0) && (N == (pq.length - 1) / 4)) resize(pq.length / 2);
return key;
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public static void main(String[] args) {
MaxPQ<String> pq = new MaxPQ<String>();
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String item = in.nextLine();
if (!item.equals("-")) pq.insert(item);
else if (!pq.isEmpty()) System.out.print(pq.delMax() + " ");
}
System.out.println("(" + pq.size() + " left on pq)");
}
}
算法复杂度
对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过lgN+1次比较,删除最大元素的操作需要不超过2lgN次比较。
变化
多叉堆
例如三叉堆,对于数组中1至N的N个元素,位置k的结点大于等于位于3k-1、3k和3k+1的结点,小于等于位于的结点。将其修改为任意的d叉树也并不困难。我们需要在树高和在每个结点的d个子结点找到最大者的代价之间找到折中,这取决于实现的细节以及不同啊哦做的预期相对频繁程度。
调整数组大小
当优先队列的数组大小可以调整、队列长度可以是任意值时,对数时间复杂度上限就只是针对一般性的队列长度N而言了。
元素的不可变性
优先队列存储了用例创建的对象。但同时假设用例代码不会改变它们(改变它们就可能改变堆的有序性)
索引优先队列
给每一个元素增加索引,允许用例引用已经进入优先队列中的元素。
package Sort;
public class IndexMinPQ<Item extends Comparable<Item>>{
private Item[] items;
private int[] pq;
private int[] qp;
private int N;
IndexMinPQ(int capacity){
items = (Item[]) new Comparable[capacity + 1];
pq = new int[capacity + 1];
qp = new int[capacity + 1];
for(int i = 1; i <= capacity; i++){
qp[i] = -1;
}
}
IndexMinPQ(Item[] keys){
this(keys.length);
System.arraycopy(keys, 0, items, 1, keys.length);
for(int k = N / 2; k >= 1; k--){
sink(k);
}
}
private boolean greater(int i, int j){
return items[pq[i]].compareTo(items[pq[i]]) > 0;
}
private void swap(int i, int j){
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
qp[pq[i]] = i;
qp[pq[j]] = j;
}
private void sink(int k){
while (k * 2 <= N){
int j = k * 2;
if(j < N && greater(j, j + 1)) j++;
if(!greater(k, j)) break;
swap(k, j);
k = j;
}
}
private void swin(int k){
while (k > 1 && greater(k / 2, k)){
swap(k, k / 2);
k /= 2;
}
}
public boolean contain(int k){
return qp[k] != -1;
}
public void insert(int i, Item key){
items[++N] = key;
pq[N] = i;
qp[i] = N;
swin(N);
}
public void change(int i, Item key){
items[qp[i]] = key;
swin(qp[i]);
sink(qp[i]);
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public Item minItem(){
return items[pq[1]];
}
public int delMin(){
int min = pq[1];
swap(1, N);
qp[pq[N]] = -1;
items[pq[N--]] = null;
sink(1);
return min;
}
}
索引优先队列用例
下面的用例调用了IndexMinPQ的代码Mutiway解决了多向归并问题:它将多个有序的输入流归并成一个有序的输出流。
package Sort;
public class IndexMinPQ<Item extends Comparable<Item>>{
private Item[] items;
private int[] pq;
private int[] qp;
private int N;
IndexMinPQ(int capacity){
items = (Item[]) new Comparable[capacity + 1];
pq = new int[capacity + 1];
qp = new int[capacity + 1];
for(int i = 1; i <= capacity; i++){
qp[i] = -1;
}
}
IndexMinPQ(Item[] keys){
this(keys.length);
System.arraycopy(keys, 0, items, 1, keys.length);
for(int k = N / 2; k >= 1; k--){
sink(k);
}
}
private boolean greater(int i, int j){
return items[pq[i]].compareTo(items[pq[i]]) > 0;
}
private void swap(int i, int j){
int temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
qp[pq[i]] = i;
qp[pq[j]] = j;
}
private void sink(int k){
while (k * 2 <= N){
int j = k * 2;
if(j < N && greater(j, j + 1)) j++;
if(!greater(k, j)) break;
swap(k, j);
k = j;
}
}
private void swin(int k){
while (k > 1 && greater(k / 2, k)){
swap(k, k / 2);
k /= 2;
}
}
public boolean contain(int k){
return qp[k] != -1;
}
public void insert(int i, Item key){
items[++N] = key;
pq[N] = i;
qp[i] = N;
swin(N);
}
public void change(int i, Item key){
items[qp[i]] = key;
swin(qp[i]);
sink(qp[i]);
}
public boolean isEmpty(){
return N == 0;
}
public int size(){
return N;
}
public Item minItem(){
return items[pq[1]];
}
public int delMin(){
int min = pq[1];
swap(1, N);
qp[pq[N]] = -1;
items[pq[N--]] = null;
sink(1);
return min;
}
}
堆排序
我们可以把任意优先队列变成一种排序方法。将所有元素插入一个查找最小元素的优先队列,然后再重复调用删除最小元素的操作来将它们按顺序删去。用无序数组实现的优先队列这么做相当于一次选择排序。基于堆的优先队列这样做即为堆排序。
堆排序分为两个阶段。在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中;然后在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果。
堆的构造
从右至左用sink()函数构造子堆。数组的每个位置都已经是一个子堆的根结点了,sink()对于这些子堆也适用。如果一个结点的两个子结点都已经是堆了,那么在该结点上调用sink()可以将它们变成一个堆。这个过程会递归地建立起堆的秩序。开始时我们只需要扫描数组中的一半元素,因为我们可以跳过大小为1的子堆。最后我们在位置1上sink()方法,扫描结束。
下沉排序
我们将堆中的最大元素删除,然后放入堆缩小后数组空出的位置。
实现代码
package Sort;
public class HeapSort{
private static Comparable[] pq;
private static int N;
public static void sort(Comparable[] a){
pq = new Comparable[a.length + 1];
N = a.length;
System.arraycopy(a, 0, pq, 1, a.length);
for(int k = N / 2; k >= 1; k--){
sink(k);
}
int i = 0;
while (N > 0){
a[i++] = pq[1];
swap(1, N);
pq[N--] = null;
sink(1);
}
}
private static boolean less(int i, int j){
return pq[i].compareTo(pq[j]) < 0;
}
private static void swap(int i, int j){
Comparable temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
private static void sink(int k){
while (k * 2 <= N){
int j = k * 2;
if(j < N && less(j + 1, j)) j++;
if(!less(j, k)) break;
swap(k, j);
k = j;
}
}
private static void swin(int k){
while (k > 1 && less(k, k / 2)){
swap(k, k / 2);
k /= 2;
}
}
public static void main(String[] args){
Integer[] a = new Integer[]{4,2,5,6,8,2,6,23,631,100};
sort(a);
for(int n : a){
System.out.print(n);
System.out.print(' ');
}
}
}