优先队列
定义
许多应用程序要处理许多有序的元素,但不一定要求他们全部有序;或者不要求一次就将他们排序,很多情况下我们会处理当前键值最大的元素,然后再搜集更多的元素,在处理最大的元素,或者为每一个应用分配一个优先级,并总是处理下一个优先级最高的事件。
在这种情况下,一个合适的数据结构应该支持两种操作:删除最大元素、插入元素。这种数据结构叫做优先队列。优先队列的使用和队列及栈类似。通过插入一列元素然后一个个删除最小的元素,我们可以用优先队列实现排序算法-堆排序。
API
我们要操作的元素一定是实现了Comparable接口的,
MaxPQ() :创建一个优先队列
MaxPQ():创建一个初始容量为max的优先队列。
MaxPQ(Key[] a)用数组a中的元素创建一个优先队列。,
void insert(Key v) 插入一个元素。
Key max() 返回优先队列中的最大元素
Key delMax() 删除优先队列中的最大元素。
实现
堆的定义
数据结构二叉堆能很好的实现优先队列的基本操作,在二叉堆数组中每个元素都要保证大于等于另外两个位置的元素。相应的,这些位置的元素又要至少大于等于数组另两个元素,我们将这些元素画成一颗二叉树,如图
堆有序:当一颗二叉树的每个节点都大于等于它的两个子结点时。
二叉堆(下面简称堆)表示
我们可以用链表的方式表示,这样每个结点都要维护三个引用(一个指向父节点,两个指向孩子结点)。但如果我们使用一颗完全二叉树就非常简单(如上图),我们只需要数组就可以完成,具体为将二叉树的结点按层级顺序放入数组中,根节点在1(数组索引位置为0的不用)。那么在一个堆中位置为K的结点的父节点为K/2,左右孩子结点分别为2K,2K+1。
堆的算法
我们用长度为N+1的数组pq[]来表示一个大小为N的堆,pq[0]不用。我们需要的比较和交换位置的私有辅助函数如下
private boolean less(int i,int j)
{return pq[i].compareTo(pq[j])<0;
}
private void exch(int i,int j)
{
Key key=pq[i];
pq[i]=pq[j];
pq[j]=key;
}
在有序化的过程中我们会遇到两种情况,某个结点的优先级上升,比如在堆低加入一个新的元素我们需要由下至上恢复顺序,某个结点的优先级下降,比如将根节点替换为一个优先级更小的元素我们需要由上至下恢复顺序,
由下至上恢复顺序(上浮)
如果堆的有序状态因为某个结点变得比它的父节点大而被打破,那么我们需要交换它和父节点来修复堆,交换后仍然可能比它的新的父节点大,所以需要继续交换直到遇到一个比它大的父节点或者到达了根节点。
代码实现:
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;
}
}
插入元素
我们将新元素放到数组的末尾,增加堆的大小并让这个元素上浮到合适的位置。
删除最大元素
我们从数组顶端删除最大元素并将数组的最后一个元素放到顶端,减少堆的大小并将这个元素下沉到合适的位置。
这两种操作如图所示:
完整代码:
1 public class MaxPQ<Key extends Comparable<Key>> { 2 private Key pq[]; 3 private int N=0; 4 public MaxPQ(int max) 5 { 6 pq=(Key[])new Comparable[max]; 7 } 8 9 private boolean less(int i,int j) 10 {return pq[i].compareTo(pq[j])<0; 11 } 12 13 private void exch(int i,int j) 14 { 15 Key key=pq[i]; 16 pq[i]=pq[j]; 17 pq[j]=key; 18 } 19 20 private void swim(int k) 21 { 22 while(k>1&&less(k/2,k)) 23 { 24 exch(k/2,k); 25 k=k/2; 26 } 27 } 28 29 private void sink(int k) 30 { 31 while(2*k<=N) 32 { 33 int j=2*k; 34 if(j<N&&less(j,j+1)) j++; 35 if(!less(k,j)) break; 36 exch(k,j); 37 k=j; 38 39 } 40 } 41 42 public boolean isEmpty() 43 { 44 return N==0; 45 } 46 47 public int size() 48 { 49 return N; 50 } 51 52 public void insert(Key key) 53 { 54 pq[++N]=key; 55 swim(N); 56 } 57 58 public Key delMax() 59 { 60 Key max=pq[1]; 61 exch(1,N--);//传递的参数为N,当函数执行完后再执行N--; 62 pq[N+1]=null; 63 sink(1); 64 return max; 65 66 } 67 68 69 }
注意:本代码没有考虑数组越界问题,所有操作都是理想的,不会发生异常
堆排序
对于上面的优先队列,只要我们改变一下less()函数的判断条件,就可以就可以将他变为一个删除最小元素的优先队列。
堆排序分为两个阶段,在堆的构造阶段,我们将原始数组重新组织安排到一个堆中,然后在下沉排序阶段,我们从堆中按照递减顺序取出所有元素并得到排序结果,为了排序的需要,我们将直接使用swim()和sink()方法。
堆的构造
关于如何构造堆,我们有两种方式
(1)采用swim()方法,从左往右扫描数组,这样可以保证指针左侧的元素都是堆有序的,这种方式类似于创建一个空堆,然后不断调用insert()方法。
(2)采用sink()方法,从右往左扫描数组,对于一个给定的数组,每一个元素都可以看成是一个已经排好的堆,如果一个结点的两个子节点都已经是堆了,那么在该节点上调用sink()方法也能将他们变成堆。使用这种方法我们只需要扫描一办的数组
代码实现
1 public class Heap { 2 private Heap() { } 3 4 5 public static void sort(Comparable[] pq) { 6 int n = pq.length; 7 for (int k = n/2; k >= 1; k--) 8 sink(pq, k, n); 9 while (n > 1) { 10 exch(pq, 1, n--); 11 sink(pq, 1, n); 12 } 13 } 14 15 16 17 private static void sink(Comparable[] pq, int k, int n) { 18 while (2*k <= n) { 19 int j = 2*k; 20 if (j < n && less(pq, j, j+1)) j++; 21 if (!less(pq, k, j)) break; 22 exch(pq, k, j); 23 k = j; 24 } 25 } 26 27 28 private static boolean less(Comparable[] pq, int i, int j) { 29 return pq[i-1].compareTo(pq[j-1]) < 0; 30 } 31 32 private static void exch(Object[] pq, int i, int j) { 33 Object swap = pq[i-1]; 34 pq[i-1] = pq[j-1]; 35 pq[j-1] = swap; 36 } 37 38 39 }
下沉排序:
我们将堆中最大的元素删除,然后放入堆缩小后空出的位置
算法行为: