《算法(第四版)》排序-----优先队列


  在实际应用中,我们常常不一定要求整个数组全部有序,或者不需要一次就将它们排序,可能只需要当前数组的键值最大的元素或最小的元素,这时就类似于总在处理下一个优先级最高的元素,在这种情况下一个合适的数据结构应该支持两种操作:删除最大元素插入元素。这种类型叫做优先队列

  实现这种数据结构的方法有两种,一种是简单的数组或链表来实现,一种是具有高效的二叉堆来实现,在本文中我们就是采用二叉堆来实现的。

  本文的目的是写一个实现二叉堆的API,以便后续需要优先队列的这种数据结构时使用。优先队列最重要的操作就是删除最大元素(或最小,其实差别就在于在less()函数比较方向改变一下就行)和插入元素。为了保证灵活性,我们采用泛型,将实现了Comparable接口的数据类型作为参数Key。

                                                      表:泛型 优先队列的API

              ------------------------------------------------------------------------------------------------------------ 

                                        public  class  MaxPQ<Key  extends  Comparable<Key>>             

              ------------------------------------------------------------------------------------------------------------

                                MaxPQ                                                           创建一个优先队列  

                                MaxPQ(int max)                                      创建一个初始容量为max的优先队列 

                                           MaxPQ(Key[]  a)                                      用a[]中的元素创建一个优先队列

                   ---------------------------------------------------------------------------------------------------------------------------------------

                                          void   Insert(Key  v)                                       向优先队列中插入一个元素

                                          Key   max()                                                    返回最大元素

                                          Key   delMax()                                               删除并返回最大元素

                                          boolean  isEmpty()                                      返回队列是否为空

                                          int     size()                                                     返回优先队列中的元素个数

                  -------------------------------------------------------------------------------------------------------------

上面的为MaxPQ的API,与MaxPQ类似,我们也可以一个MinPQ的API,含有一个delMin()方法来删除并返回队列中键值最小的那个元素,只需要改变less()的方向就可以了。


1.堆的定义

   在二叉堆的数组中,每个元素都要保证大于等于另两个特定位置的元素,也就是父节点一定要大于等于它的两个子节点,此时成为堆有序,由此可以退出,根节点是对有序的二叉树中的最大节点。

2.二叉堆表示方法

   我们将二叉树的节点按照层级顺序放入数组中,根节点在位置1,它的子节点在位置2和3,而子节点的子节点分别在位置4,5    6,7以此类推。简单起见,下文的二叉堆我们简称为堆。在一个对中位置k的节点的父节点为[k/2](向下取整),而它的两个子节点位置分别为2k, 2k+1,这样在不适用指针情况下就在数组中实现了二叉堆。

一颗大小为N的完全二叉树的高度为[lgN] 下取整   (2 ^ h= N  ,所以h=lgN)

                                   

3.堆的算法

我们用长度为N+1的私有数组pq[]来表示大小为N的对(N+1的原因是为了满足二叉树的结构关系,不会用到pq[0])堆元素放在pq[1]到pq[N]中。在排序算法中我们只通过less()和exchange()来访问元素,因为元素都在pq[]中,所以此时传入的参数不像之前博客的几篇排序算法中的less()和exch()传入整个数组,仅仅传入索引,具体实现如下

	private boolean less(int i, int j) {
		return pq[i].compareTo(pq[j]) < 0;
	}
	private void exch(int i, int j) {
		Key temp = pq[i];pq[i] = pq[j];pq[j]=temp;
	}
(1)由下至上的堆有序化(上浮)

当插入一个元素时,我们一般先放到数组的末尾,然后让它一点点向上移动,直到移动到它的父节点比他大,这种操作叫上浮swim(),这样一趟后,数组重新恢复有序性

rivate void swim(int k) {
	while(k <=1 && less(k/2, k)){  //不断的向上移动,直到已经是最高父节点或他的父节点比他大
		exch(k/2, k);
		k = k/2;
	}
		
}

   



(2)由下至上的堆有序化(下沉)

一般删除最大键值的方式是将最大键值与数组的末尾进行交换,然后把最大键值的指向null,可是由于数组的末尾肯定不是最大的父节点,所以需要将它一点点向下移动,直到移动到它的子节点比它小为止,这种操作就叫下沉  sink(),这样一趟后,数组重新恢复有序性

private void sink(int k) {
	while(k*2 <= N){                      //有两种跳出循环方式,一种是已经到底了,另一种就是下面的不在比他小
		int j = k * 2;
		if(j < N && less(j, j+1)) j++;  //一般让这个节点和他子节点的最大值比较,这样减少交换次数,所以j++
		if(!(less(k, j)))  break;     //此处就是不比他小时跳出
		exch(k ,j);
		k = j;
	}
}
 

  其实可以把元素的上浮和下沉理解为一个公司,当来了一个新人(insert)时,就先把他放到最底层,然后发现他比别人好,那就一点点给他升级,知道上升到他的上级比他还好为止,这就是上浮,而一个公司的大boss走了,此时由于他的下级有两个人,不知道让谁上去好,为了平衡整个管理体系,大boss出了一招,他和最末尾的员工换位置,最末尾的员工显然胜任不了最高级的位置,所以他就一点点的向下移动,知道他的下级比他还差,这叫下沉。


整体的代码实现如下

public class MaxPQ<Key extends Comparable<Key>> {
	
	private Key[] pq;    //基于堆的完全二叉树
	private int N = 0;   //存储于pq[1...N]中,pq[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 k){
		pq[++N] = k;      //插入一个数组,N就加一
		swim(N);          //上浮,恢复堆的有序性
	}
	private void swim(int k) {
		while(k <=1 && less(k/2, k)){
			exch(k/2, k);
			k = k/2;
		}
		
	}
	private void exch(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;
	}
	public Key delMax(){
		Key max = pq[1];    //从根节点取到最大元素,最高父节点
		exch(1, N--);       //让根节点和数组的最后一个节点交换,同时将N自减一
		pq[N+1] = null;     //将最大元素指向空
		sink(1);            //下沉,恢复堆的有序性
		return max;
	}
	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;
			exch(k ,j);
			k = j;
		}
	}
	
}

4优先队列调用示例

问题:输入N个字符串,每个字符串都对应着一个整数,你的任务就是从中找出最大的(或最小的)M个整数(及其相关联的字符串),在某些禅境中输入的量可能非常巨大,甚至可以认为是无限的,解决这个方法如果先排序,然后在找,或者不断的替换最大元素,这种比较的代价会非常高昂,所以此时可使用优先队列。

                   表: 从N个输入中找到最大的M个元素所需要的成本

-------------------------------------------------------------------------------------------

                                                              增长的数量级

                       示     例          ---------------------------------------------------------------------

                                                                   时    间                           空     间

-----------------------------------------------------------------------------------------------------------------

       排序算法的用例                               NlogN                                N              

       调用简单实现的优先队列                NM                                    M             

       调用基于堆实现的优先队列           NlogM                               M
-----------------------------------------------------------------------------------------------------------------

调用的示例如下:


从命令行输入一个整数M,从输入流种获得一系列字符串,输入流的每一行代表一个交易,这段代码调用了MinPQ并会打印数字最大的M行,当优先队列的大小超过M时就删掉其中最小的元素,处理完所有交易,优先队列中方正以增煦排列的最大的M个交易。


5.多叉堆

基于用数组表示的完全三叉树构造堆并修改相应的代码并不难,对应数组中1至N的N个元素,位置k的结点大于大于等于3k-1,3k,3k+1的结点,小于位于[(k+1)/3]下取整的结点。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值