数据结构及算法基础--优先队列(Priority Queue)
这真的是一个包含很多东西的数据结构。我们会逐步解析,来讲解优先队列:
首先知道什么是优先队列:
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除(first in, last out)。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
然后,我们给出其API,
在《algorithm》书中提出了一个很有趣的情况,如果我们从N个元素中选出最大的M个元素,那么其时间复杂度是多少。
书中讨论了三种方法:
1)通过排序得出,但是我们可以很明确的明白如果N过大,这个方法需要消耗很长时间。
2)如果我们使用基础优先队列,我们在insert新元素的时候,和存在的五个元素进行比较,那么这个时间复杂度将是MN;
3)最后一种情况,如果我们用堆的优先序列,这个时候,因为堆的结构,我们便可以使时间复杂度便为NlogM。至于堆的具体结构,我们在后面会提出。
三种情况的空间和时间复杂度如下:
我们的优先序列的基础实现有两种方法,一种是使用排序序列,一种是无序序列
1)array表示(无序),对于insert操作来说,和栈操作中的push一致;对于remove the maximum操作,我们则需要在其中加入类似于选择排序的循环代码,将最大的数放在array最后,然后删除
2)array表示(无序),对于insert操作来说,我们在其中加入插入排序的代码,确保每个加入的代码都在正确的位置;对于remove the maximum操作,和栈中pop操作类似。
各自的流程可以如下图所示:
其实还有一种表示方法在书中也有提出,便是通过链表表示,同样通过改变pop的代码来返回并删除最大值。
以上的方法其实都不是最好的效果,我们在这里会使用heap-based Priority Queue便是主要考虑insert 和remove方法的复杂度,我们使用它,可以保证两个方法的复杂度都很好,通过下图便可以做出比较
这里给出无序数组实现的代码实现:
package PriorityQueue; public class UnorderedArrayMaxPQ<Key extends Comparable<Key>> { private Key[] pq; private int N; public UnorderedArrayMaxPQ(int capacity){ pq=(Key[]) new Comparable[capacity]; N=0; } public boolean isEmpty(){ return N==0; } public void insert(Key x){ pq[N++]=x; } public Key delMax(){ int max=0; for(int i=1;i<N;i++){ if(less(max,i))max=i; } exch(max,N-1); return pq[--N]; } 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; } }
二分堆表示PQ
接下来就是很重点的东西了。首先我们知道堆(heap)其实是树(tree)的数组对象。堆最重要的是有两个性质:
-
堆中某个节点的值总是不大于或不小于其父节点的值;
-
堆总是一棵完全二叉树。
我们在这里不在深入讲解堆的知识,而知识给出基本概念,以便我们能够在理解heap-based priority queue的时候更有效率。
同时有一个非常重要的地方,为了我们之后进行swim和sink操作的便捷和准确,在heap中,数组的第一个位置会一直是空的。具体的例子如下:
这里有个关于树的定理在之前提到过,树的高度=lgN。
接下来讨论堆的算法:
其中最重要的便是swim和sink方法的处理。
1)从底向上重新堆(Button-Up reheapify)也就是swim
我们需要讲较大的节点向上移动到合适位置,才能满足堆的结构要求
swim的过程如下图所示:
由于堆的特殊结构,如果子节点在位置k上,那么其父节点位置一定在k/2上。这是必然的。
那么,在代码实现的过程中,我们就可以很轻易的实现:
private void swim(int k){ while(k>1&&less(k/2,k)){ exch(k/2,k); k=k/2; }
2)从顶向下重新堆(Top-down reheapify)也就是sink
其思想和swim一样,将较小的节点下移到合适的位置。过程思想如下:
同样,在堆结构中,一个节点的位置是k,那么其子节点位置一定是2*k和2*k+1;我们选取自己较大的一方作为sink的目标。
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; } }
当我们完成这两个方法的时候,就我们的insert和remove the max方法便已经变的很简单了,所以整个priority queue的代码如下:
package PriorityQueue; public class MaxPQ<Key extends Comparable<Key>> { private Key[] pq; private int N; //in key[1....n] which the key[0] unused;see in the structure of heap-based PQ; public MaxPQ(int maxN){ pq=(Key[])new Comparable[maxN]; } 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; // avoiding loitering; 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; } } }
这里有个定理:
对于N个元素的堆,在insert中我们不会使用超过1+lgN次比较,在remove the max中不会使用超过2lgN次比较。
我们同样以上面的过程为例子给出heap-based Priority Queue的流程:
接下来,就是一个较难的内容,索引优先队列
优先队列有一个缺点,就是不能直接访问已存在于优先队列中的对象,并更新它们。这个问题在Dijistra算法中就有明显的体现,有时候我们需要更新已在队列中的顶点的距离。为此就需要设计一种新型的数据结构来解决这个问题,这就是本文要介绍的索引优先队列。
索引优先队用一个整数和对象进行关联,当我们需要跟新该对象的值时,可以通这个整数进行快速索引,然后对对象的值进行更新。当然更新后的对象在优先队列中的位置可能发生变化,这样以保证整个队列还是一个优先队列。
因为索引优先队列的理解对我来说有点小困难,我们这里给出一个例子:
考虑下面一种情况,比如李雷考了全班第一,韩梅梅考了第二。我们把全班四十个人的成绩按照高低排了优先队列。但是复核的时候,突然发现韩梅梅的成绩少算了10分,加上10分应该她是第一。那么,如果没有这个索引,我们要怎么修改已经形成的优先队列呢?
有的人可能说,很简单啊,把李雷和韩梅梅出队列,然后更改成绩,重新加入队列。
好,那么,假如韩梅梅成绩统计错了,她是全班第三十九人呢?难道要把39个人的成绩重新出队列,然后重新加入吗?这个成本代价似乎有点高。
更进一步,如果是全校四千人的队列呢?如果有一千人的成绩全算错了呢?我们要重新生成这个队列一千次?
这时候,索引优先队列就有了用武之地。如果韩梅梅的成绩错了,我们从索引里知道她是优先队列里的第二个,那么我们直接修改她的成绩,然后上浮或者下沉就可以了,要付出的代价非常小。
这个例子是我找到的最符合也对我最好理解的运用索引优先队列的例子了。
上图可以表示出index priority queue的三个数组
运用上面的例子,pq表示每个排名上分别是谁,qp表示每个人分别是多少名,elements则表示每个人分别是多少分,
大致的结构和运用都可以参考这个例子。
那么,index priority queue的API如下图所示:
当然,这是一个最小索引优先队列,最大优先队列其实思想也是一样的。
下面我们运用书中给出的非常详尽的index priority queue的实现,该算法实现的是最大index priority queue
package PriorityQueue; /****************************************************************************** * Compilation: javac IndexMaxPQ.java * Execution: java IndexMaxPQ * Dependencies: StdRandom.java * * Maximum-oriented indexed PQ implementation using a binary heap. * ******************************************************************************/ import java.lang.Integer; import java.util.Iterator; import java.util.NoSuchElementException; /** * The {@code IndexMaxPQ} class represents an indexed priority queue of generic keys. * It supports the usual <em>insert</em> and <em>delete-the-maximum</em> * operations, along with <em>delete</em> and <em>change-the-key</em> * methods. In order to let the client refer to items on the priority queue, * an integer between {@code 0} and {@code maxN - 1} * is associated with each key—the client * uses this integer to specify which key to delete or change. * It also supports methods for peeking at a maximum key, * testing if the priority queue is empty, and iterating through * the keys. * <p> * This implementation uses a binary heap along with an array to associate * keys with integers in the given range. * The <em>insert</em>, <em>delete-the-maximum</em>, <em>delete</em>, * <em>change-key</em>, <em>decrease-key</em>, and <em>increase-key</em> * operations take logarithmic time. * The <em>is-empty</em>, <em>size</em>, <em>max-index</em>, <em>max-key</em>, * and <em>key-of</em> operations take constant time. * Construction takes time proportional to the specified capacity. * <p> * For additional documentation, see <a href="https://algs4.cs.princeton.edu/24pq">Section 2.4</a> of * <i>Algorithms, 4th Edition</i> by Robert Sedgewick and Kevin Wayne. * * @author Robert Sedgewick * @author Kevin Wayne * * @param <Key> the generic type of key on this priority queue */ public class IndexMaxPQ<Key extends Comparable<Key>> implements Iterable { private int n; //number of elements of pq; private int[] pq; //binary heap using 1-based index; private int[] qp; //inverse of pq--pq[qp[i]]=i; private Key[] key; //key[i] /** * Initializes an empty indexed priority queue with indices between {@code 0} * and {@code maxN - 1}. * * @param maxN the keys on this priority queue are index from {@code 0} to {@code maxN - 1} * @throws IllegalArgumentException if {@code maxN < 0} */ public IndexMaxPQ(int maxN){ if(maxN<0)throw new IllegalArgumentException(); n=0; pq=new int[maxN+1]; qp=new int[maxN+1]; key=(Key[])new Comparable[maxN+1]; for(int i=0;i<=maxN;i++) qp[i]=-1; } /** * Returns true if this priority queue is empty. * * @return {@code true} if this priority queue is empty; * {@code false} otherwise */ public boolean isEmpty(){ return n==0; } /** * Is {@code i} an index on this priority queue? * * @param i an index * @return {@code true} if {@code i} is an index on this priority queue; * {@code false} otherwise * @throws IllegalArgumentException unless {@code 0 <= i < maxN} */ public boolean contains(int i){ return qp[i]!=-1; } /** * Returns the number of keys on this priority queue. * * @return the number of keys on this priority queue */ public int size(){ return n; } /** * Associate key with index i. * * @param i an index * @param key the key to associate with index {@code i} * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws IllegalArgumentException if there already is an item * associated with index {@code i} */ public void insert(int i,Key key){ if(contains(i))throw new IllegalArgumentException("index is already exist"); n++; qp[i]=n; pq[n]=i; this.key[i]=key; swim(n); } /** * Returns an index associated with a maximum key. * * @return an index associated with a maximum key * @throws NoSuchElementException if this priority queue is empty */ public int maxIndex(){ if(n==0)throw new NoSuchElementException("Priorty queue underflow"); return pq[1]; } /** * Returns a maximum key. * * @return a maximum key * @throws NoSuchElementException if this priority queue is empty */ public Key maxKey(){ if(n==0)throw new NoSuchElementException("Priorty queue underflow"); return key[pq[1]]; } /** * Removes a maximum key and returns its associated index. * * @return an index associated with a maximum key * @throws NoSuchElementException if this priority queue is empty */ public int delMax(){ if(n==0)throw new NoSuchElementException("Priorty queue underflow"); int max=pq[1]; exch(1,n--); sink(1); assert pq[n+1]==max; qp[max]=-1; key[max]=null; pq[n+1]=-1; return max; } /** * Returns the key associated with index {@code i}. * * @param i the index of the key to return * @return the key associated with index {@code i} * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws NoSuchElementException no key is associated with index {@code i} */ public Key keyOf(int i){ if(!contains(i))throw new IllegalArgumentException("index is not in the priority queue"); return key[i]; } /** * Change the key associated with index {@code i} to the specified value. * * @param i the index of the key to change * @param key change the key associated with index {@code i} to this key * @throws IllegalArgumentException unless {@code 0 <= i < maxN} */ public void changeKey(int i,Key key){ if(!contains(i))throw new IllegalArgumentException("index is not in the priority"); this.key[i]=key; swim(qp[i]); sink(qp[i]); } /** * Change the key associated with index {@code i} to the specified value. * * @param i the index of the key to change * @param key change the key associated with index {@code i} to this key * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @deprecated Replaced by {@code changeKey(int, Key)}. */ public void change(int i,Key key){ changeKey(i,key); } /** * Increase the key associated with index {@code i} to the specified value. * * @param i the index of the key to increase * @param key increase the key associated with index {@code i} to this key * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws IllegalArgumentException if {@code key <= keyOf(i)} * @throws NoSuchElementException no key is associated with index {@code i} */ public void increaseKey(int i,Key key){ if(!contains(i))throw new IllegalArgumentException("index is not in the priority"); if(this.key[i].compareTo(key)>=0)throw new IllegalArgumentException("Calling increasKey() with given" + " argument would not strictly increase the key"); this.key[i]=key; swim(qp[i]); } /** * Decrease the key associated with index {@code i} to the specified value. * * @param i the index of the key to decrease * @param key decrease the key associated with index {@code i} to this key * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws IllegalArgumentException if {@code key >= keyOf(i)} * @throws NoSuchElementException no key is associated with index {@code i} */ public void decreaseKey(int i,Key key){ if(!contains(i))throw new IllegalArgumentException("index is not in the priority"); if(this.key[i].compareTo(key)<=0)throw new IllegalArgumentException("Calling decreasKey() with given" + " argument would not strictly decrease the key"); } /** * Remove the key on the priority queue associated with index {@code i}. * * @param i the index of the key to remove * @throws IllegalArgumentException unless {@code 0 <= i < maxN} * @throws NoSuchElementException no key is associated with index {@code i} */ public void delete(int i){ if(!contains(i))throw new IllegalArgumentException("index is not in the priority"); int index=qp[i]; exch(index,n); n--; swim(index); sink(index); key[i]=null; qp[i]=-1; } /************************************** * General Helper Functions ************************************** */ public boolean less(int i,int j){ return key[pq[i]].compareTo(key[pq[j]])<0; } public void exch(int i,int j){ int swap=pq[i]; pq[i]=pq[j]; pq[j]=swap; qp[pq[i]]=i; qp[pq[j]]=j; } /************************************** * Heap Helper Functions ************************************** */ public void swim(int k){ while(k>1&&less(k/2,k)){ exch(k/2,k); k=k/2; } } public 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 Iterator iterator(){ return new HeapIterator(); } /** * Returns an iterator that iterates over the keys on the * priority queue in descending order. * The iterator doesn't implement {@code remove()} since it's optional. * * @return an iterator that iterates over the keys in descending order */ private class HeapIterator implements Iterator{ private IndexMaxPQ<Key> copy; public HeapIterator(){ copy=new IndexMaxPQ<Key>(pq.length-1); for(int i=1;i<=n;i++) copy.insert(pq[i], key[pq[i]]); } public boolean hasNext(){return !copy.isEmpty();} public void remove(){throw new UnsupportedOperationException();} public Integer next(){ if(!hasNext())throw new NoSuchElementException(); return copy.delMax(); } } /** * Unit tests the {@code IndexMaxPQ} data type. * * @param args the command-line arguments */ public static void main(String[] args) { // insert a bunch of strings String[] strings = { "it", "was", "the", "best", "of", "times", "it", "was", "the", "worst" }; IndexMaxPQ<String> pq = new IndexMaxPQ<String>(strings.length); for (int i = 0; i < strings.length; i++) { pq.insert(i, strings[i]); } // print each key using the iterator for (int i:pq.pq) { System.out.println(i + " " + strings[i]); } System.out.println(); // increase or decrease the key for (int i = 0; i < strings.length; i++) { if (StdRandom.uniform() < 0.5) pq.increaseKey(i, strings[i] + strings[i]); else pq.decreaseKey(i, strings[i].substring(0,1)); } // delete and print each key while (!pq.isEmpty()) { String key = pq.maxKey(); int i = pq.delMax(); System.out.println(i + " " + key); } System.out.println(); // reinsert the same strings for (int i = 0; i < strings.length; i++) { pq.insert(i, strings[i]); } // delete them in random order int[] perm = new int[strings.length]; for (int i = 0; i < strings.length; i++) perm[i] = i; // StdRandom.shuffle(perm); // System.out.print(perm.length); for (int i = 0; i < perm.length; i++) { String key = pq.keyOf(i); pq.delete(i); System.out.println(perm[i] + " " + key); } } }
抛开本文的优先队列的学习不说,这个代码也有非常非常多值得和需要我们去学习的地方,包括对annotations和注释的合理使用,程序的优化和测试的内容,都非常非常的棒。
这也是一个完美的索引优先队列。
我们对索引优先队列很重要的定理:
对于任何大小为N的队列,我们需要的比较次数,在insert,delete,change prioirty和remove max中,都不超过logN
这种,我们就需要讨论索引优先队列中各方法的算法复杂度。其最差情况具体如下图所示: