算法——优先队列

许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或是不一定要一次就将它们排序。很多情况下我们会收集一些元素,处理当前键值最大的元素,然后再收集更多的元素,再处理当前键值最大的元素,如此这般。例如,你可能有一台能够同时运行多个应用程序的电脑。这是通过每个应用程序的事件分配一个优先级,并总是处理下一个优先级最高的事件来实现的。例如,绝大多数手机分配给来电的优先级都会比游戏程序高。

通俗来说,当收集元素永远不会停止,也就是相当于有无数个元素,那么我们将永远比较不完所有元素,我们也许只会使用其中最大的十个元素,所以我们只需要把新进入队列的元素与这十个元素比较,去除最小的一个元素即可。

在这种情况下,一个合适的数据结构应该支持两种操作,删除最大元素插入元素。这种数据类型叫做有限队列。

优先队列是一种抽象数据类型,API如下(Key为泛型):

public class MaxPQ<Key extends Comparable<Key>>
MaxPQ()                                         创建一个优先队列       
    MaxPQ(int max)                            创建一个初始容量为max的优先队列
  MaxPQ(Key [] a)                            用a[]中元素创建一个优先队列
void insert(key v)                           向优先队列插入一个元素
Key max()                                        返回最大元素
  Key dekMax()                                删除并返回最大元素
boolean isEmpty()                        返回队列是否为空
int size()                                         返回优先队列中的元素个数

一个优先队列的用例:

public class TopM{
	public static void main(String []args) {
		//打印输入流中的最大的M行
		int M=Integer.parseInt(args[0]);
		MinPQ<Transaction> pq=new MinPQ<Transaction>(M+1);
		while(StdIn.hasNextLine()) {
			//为下一行输入创建一个元素并放入优先队列中
			pq.insert(new Transaction(StdIn.readLine));
			if(pq.size()>M) {
				pq.delMin();//如果优先队列中存在M+1个元素则删除其中最小的元素
			}//最大的M的元素都在优先队列中
			Stack<Transaction> stack=new Stack<Transaction>();
			while(!pq,isEmpty) stack.push(pq.delMin());
			for(Transaction t:stack)StdOut.println(t);
		}
	}
}

分析:先从输入流获取一个整数M,将保存M个最大元素,之后不断从输入流中获取新的元素,与旧的M个元素比较,删除其中最小的一个元素,保证MinPQ中一直存有最大的M个元素。

初级实现

数组实现(无序):删除时,将待删除元素与边界元素互换,再删除;

数组实现(有序):按序排列,如删除最大,永远删除边界值;

列表表示法:可以用基于链表的下压站的代码作为基础,逆序排列,pop()实现删除。

对比:实现栈和队列与实现优先队列最大的不同在于对性能的要求。对于栈和队列,我们的实现能够在常数时间实现内完成所有操作;而对于优先队列,我们刚刚讨论过的所有初级实现中,插入元素和删除最大元素这两个操作之一在最坏情况下需要线性时间来完成,但是基于数据结构堆的实现能够保证这两种操作都能更快的执行(对数级别)。

堆的定义:在二叉树的数组中,每个元素都要保证大于等于另两个特定位置的元素。在树中,即每个节点都大于它的两个子叶。

二叉堆的表示:如果使用链表表示,每个元素都需要三个指针;但是如果使用完全二叉树,用数组就可以很容易表示。完全二叉树存储在数组中,根结点存储在a[1],其他结点按照从上向下,从左到右的顺序依次存储,要注意的是a[0]不存储结点信息。

位置K的结点的父结点的位置为(k/2)向下取整,而它的两个子结点的位置分别为2K和2K+1;

一棵大小为N的完全二叉树的高度为(lgN)向下取整。

堆的算法

堆的操作首先进行一些简单的改动,打破堆的状态,然后再遍历堆并按照要求将堆的状态恢复。我们把这个过程叫做堆的有序化。

private boolean less(int i,int j){     //比较i和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;
}

由下至上的对有序化(上浮swim):交换它和他的父结点来修复堆。

private void swim(int k){
    while(k>1&&less(k/2,k))
{
    exch(k/2,k);
    k=k/2;
}
}

由上至下的堆有序变化(下沉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;
      each(k,j);
      k=j;
}
}

性能:对于一个含有N个元素的基于堆的有限队列,插入元素操作只需要不超过(lgN+1)次比较,删除最大元素的操作需要不超过2lgN次比较。即可在对数时间内完成。

改进:①多叉堆

            ②调整数组大小

            ③元素的不可变性

            ④索引优先序列

猜你喜欢

转载自my.oschina.net/u/3786691/blog/1629699