PriorityQueue
基本概念
顾名思义,PriorityQueue是优先级队列,它首先实现了队列接口(Queue),与LinkedList类似,它的队列长度也没有限制,与一般队列的区别是,它有优先级的概念,每个元素都有优先级,队头的元素永远都是优先级最高的。
PriorityQueue内部是用堆实现的,内部元素不是完全有序的,不过,逐个出队会得到有序的输出。
虽然名字叫优先级队列,但也可以将PriorityQueue看做是一种比较通用的实现了堆的性质的数据结构,可以用PriorityQueue来解决适合用堆解决的问题,下一节我们会来看一些具体的例子。
基本用法
Queue接口
PriorityQueue实现了Queue接口,我们在LinkedList一节介绍过Queue,为便于阅读,这里重复下其定义:

public interface Queue<E> extends Collection<E> { boolean add(E e); boolean offer(E e); E remove(); E poll(); E element(); E peek(); }

Queue扩展了Collection,主要操作有三个:
- 在尾部添加元素 (add, offer)
- 查看头部元素 (element, peek),返回头部元素,但不改变队列
- 删除头部元素 (remove, poll),返回头部元素,并且从队列中删除
构造方法
PriorityQueue有多个构造方法,如下所示:

public PriorityQueue() public PriorityQueue(int initialCapacity) public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) public PriorityQueue(Collection<? extends E> c) public PriorityQueue(PriorityQueue<? extends E> c) public PriorityQueue(SortedSet<? extends E> c)

PriorityQueue是用堆实现的,堆物理上就是数组,与ArrayList类似,PriorityQueue同样使用动态数组,根据元素个数动态扩展,initialCapacity表示初始的数组大小,可以通过参数传入。对于默认构造方法,initialCapacity使用默认值11。对于最后三个构造方法,它们接受一个已有的Collection,数组大小等于参数容器中的元素个数。
与TreeMap/TreeSet类似,为了保持一定顺序,PriorityQueue要求,要么元素实现Comparable接口,要么传递一个比较器Comparator:
- 对于前两个构造方法和接受Collection参数的构造方法,要求元素实现Comparable接口。
- 第三个构造方法明确传递了Comparator。
- 对于最后两个构造方法,参数容器有comparator()方法,PriorityQueue使用和它们一样的,如果返回的comparator为null,则也要求元素实现Comparable接口。
基本例子
我们来看个基本的例子:

Queue<Integer> pq = new PriorityQueue<>(); pq.offer(10); pq.add(22); pq.addAll(Arrays.asList(new Integer[]{ 11, 12, 34, 2, 7, 4, 15, 12, 8, 6, 19, 13 })); while(pq.peek()!=null){ System.out.print(pq.poll() + " "); }

代码很简单,添加元素,然后逐个从头部删除,与普通队列不同,输出是从小到大有序的:
2 4 6 7 8 10 11 12 12 13 15 19 22 34
如果希望是从大到小呢?传递一个逆序的Comparator,将第一行代码替换为:
Queue<Integer> pq = new PriorityQueue<>(11, Collections.reverseOrder());
输出就会变为:
34 22 19 15 13 12 12 11 10 8 7 6 4 2
任务队列
我们再来看个例子,模拟一个任务队列,定义一个内部类Task表示任务,如下所示:

static class Task { int priority; String name; public Task(int priority, String name) { this.priority = priority; this.name = name; } public int getPriority() { return priority; } public String getName() { return name; } }

Task有两个实例变量,priority表示优先级,值越大优先级越高,name表示任务名称。
Task没有实现Comparable,我们定义一个单独的静态成员taskComparator表示比较器,如下所示:

private static Comparator<Task> taskComparator = new Comparator<Task>() { @Override public int compare(Task o1, Task o2) { if(o1.getPriority()>o2.getPriority()){ return -1; }else if(o1.getPriority()<o2.getPriority()){ return 1; } return 0; } };

下面来看任务队列的示例代码:

Queue<Task> tasks = new PriorityQueue<Task>(11, taskComparator); tasks.offer(new Task(20, "写日记")); tasks.offer(new Task(10, "看电视")); tasks.offer(new Task(100, "写代码")); Task task = tasks.poll(); while(task!=null){ System.out.print("处理任务: "+task.getName() +",优先级:"+task.getPriority()+"\n"); task = tasks.poll(); }

代码很简单,就不解释了,输出任务按优先级排列:
处理任务: 写代码,优先级:100 处理任务: 写日记,优先级:20 处理任务: 看电视,优先级:10
实现原理
理解了PriorityQueue的用法和特点,我们来看其具体实现代码,从内部组成开始。
内部组成
内部有如下成员:
private transient Object[] queue; private int size = 0; private final Comparator<? super E> comparator; private transient int modCount = 0;
queue就是实际存储元素的数组。size表示当前元素个数。comparator为比较器,可以为null。modCount记录修改次数,在介绍第一个容器类ArrayList时已介绍过。
如何实现各种操作,且保持堆的性质呢?我们来看代码,从基本构造方法开始。
基本构造方法
几个基本构造方法的代码是:

public PriorityQueue() { this(DEFAULT_INITIAL_CAPACITY, null); } public PriorityQueue(int initialCapacity) { this(initialCapacity, null); } public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; }

代码很简单,就是初始化了queue和comparator。
下面介绍一些操作的代码,大部分的算法和图示,我们在上节已经介绍过了。
添加元素 (入队)
代码为:

public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e); return true; }

offer方法的基本步骤为:
- 首先确保数组长度是够的,如果不够,调用grow方法动态扩展。
- 增加长度 (size=i+1)
- 如果是第一次添加,直接添加到第一个位置即可 (queue[0]=e)。
- 否则将其放入最后一个位置,但同时向上调整,直至满足堆的性质 (siftUp)
有两步复杂一些,一步是grow,另一步是siftUp,我们来细看下。
grow方法的代码为:

private void grow(int minCapacity) { int oldCapacity = queue.length; // Double size if small; else grow by 50% int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1)); // overflow-conscious code if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); queue = Arrays.copyOf(queue, newCapacity); }

如果原长度比较小,大概就是扩展为两倍,否则就是增加50%,使用Arrays.copyOf方法拷贝数组。
siftUp的基本思路我们在上节介绍过了,其实际代码为:

private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else siftUpComparable(k, x); }

根据是否有comparator分为了两种情况,代码类似,我们只看一种:

private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }

参数k表示插入位置,x表示新元素。k初始等于数组大小,即在最后一个位置插入。代码的主要部分是:往上寻找x真正应该插入的位置,这个位置用k表示。
怎么找呢?新元素(x)不断与父节点(e)比较,如果新元素(x)大于等于父节点(e),则已满足堆的性质,退出循环,k就是新元素最终的位置,否则,将父节点往下移(queue[k]=e),继续向上寻找。这与上节介绍的算法和图示是对应的。
查看头部元素
代码为:
public E peek() { if (size == 0) return null; return (E) queue[0]; }
就是返回第一个元素。
删除头部元素 (出队)
代码为:

public E poll() { if (size == 0) return null; int s = --size; modCount++; E result = (E) queue[0]; E x = (E) queue[s]; queue[s] = null; if (s != 0) siftDown(0, x); return result; }

返回结果result为第一个元素,x指向最后一个元素,将最后位置设置为null (queue[s] = null),最后调用siftDown将原来的最后元素x插入头部并调整堆,siftDown的代码为:

private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); }

同样分为两种情况,代码类似,我们只看一种:

private void siftDownComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) c = queue[child = right]; if (key.compareTo((E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = key; }

k表示最终的插入位置,初始为0,x表示原来的最后元素。代码的主要部分是:向下寻找x真正应该插入的位置,这个位置用k表示。
怎么找呢?新元素key不断与较小的孩子比较,如果小于等于较小的孩子,则已满足堆的性质,退出循环,k就是最终位置,否则将较小的孩子往上移,继续向下寻找。这与上节介绍的算法和图示也是对应的。
解释下其中的一些代码:
- k<half,表示的是,编号为k的节点有孩子节点,没有孩子,就不需要继续找了。
- child表示较小的孩子编号,初始为左孩子,如果有右孩子(编号right)且小于左孩子则child会变为right。
- c表示较小的孩子节点。
查找元素
代码为:
public boolean contains(Object o) { return indexOf(o) != -1; }
indexOf的代码为:

private int indexOf(Object o) { if (o != null) { for (int i = 0; i < size; i++) if (o.equals(queue[i])) return i; } return -1; }

代码很简单,就是数组的查找。
根据值删除元素
也可以根据值删除元素,代码为:

public boolean remove(Object o) { int i = indexOf(o); if (i == -1) return false; else { removeAt(i); return true; } }

先查找元素的位置i,然后调用removeAt进行删除,removeAt的代码为:

private E removeAt(int i) { assert i >= 0 && i < size; modCount++; int s = --size; if (s == i) // removed last element queue[i] = null; else { E moved = (E) queue[s]; queue[s] = null; siftDown(i, moved); if (queue[i] == moved) { siftUp(i, moved); if (queue[i] != moved) return moved; } } return null; }

如果是删除最后一个位置,直接删即可,否则移动最后一个元素到位置i并进行堆调整,调整有两种情况,如果大于孩子节点,则向下调整,否则如果小于父节点则向上调整。
代码先向下调整(siftDown(i, moved)),如果没有调整过(queue[i] == moved),可能需向上调整,调用siftUp(i, moved)。
如果向上调整过,返回值为moved,其他情况返回null,这个主要用于正确实现PriorityQueue迭代器的删除方法,迭代器的细节我们就不介绍了。
构建初始堆
如果从一个既不是PriorityQueue也不是SortedSet的容器构造堆,代码为:
private void initFromCollection(Collection<? extends E> c) { initElementsFromCollection(c); heapify(); }
initElementsFromCollection的主要代码为:

private void initElementsFromCollection(Collection<? extends E> c) { Object[] a = c.toArray(); if (a.getClass() != Object[].class) a = Arrays.copyOf(a, a.length, Object[].class); this.queue = a; this.size = a.length; }

主要是初始化queue和size。
heapify的代码为:
private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }
与之前算法一样,heapify也在上节介绍过了,就是从最后一个非叶节点开始,自底向上合并构建堆。
如果构造方法中的参数是PriorityQueue或SortedSet,则它们的toArray方法返回的数组就是有序的,就满足堆的性质,就不需要执行heapify了。
PriorityQueue特点分析
PriorityQueue实现了Queue接口,有优先级,内部是用堆实现的,这决定了它有如下特点:
- 实现了优先级队列,最先出队的总是优先级最高的,即排序中的第一个。
- 优先级可以有相同的,内部元素不是完全有序的,如果遍历输出,除了第一个,其他没有特定顺序。
- 查看头部元素的效率很高,为O(1),入队、出队效率比较高,为O(log2(N)),构建堆heapify的效率为O(N)。
- 根据值查找和删除元素的效率比较低,为O(N)。
PriorityQueue的更多相关文章
- 《徐徐道来话Java》:PriorityQueue和最小堆
在讲解PriorityQueue之前,需要先熟悉一个有序数据结构:最小堆. 最小堆是一种经过排序的完全二叉树,其中任一非终端节点数值均不大于其左孩子和右孩子节点的值. 可以得出结论,如果一棵二叉树满足 ...
- 计算机程序的思维逻辑 (46) - 剖析PriorityQueue
上节介绍了堆的基本概念和算法,本节我们来探讨堆在Java中的具体实现类 - PriorityQueue. 我们先从基本概念谈起,然后介绍其用法,接着分析实现代码,最后总结分析其特点. 基本概念 顾名思 ...
- 计算机程序的思维逻辑 (47) - 堆和PriorityQueue的应用
45节介绍了堆的概念和算法,上节介绍了Java中堆的实现类PriorityQueue,PriorityQueue除了用作优先级队列,还可以用来解决一些别的问题,45节提到了如下两个应用: 求前K个最大 ...
- jdk源码分析PriorityQueue
一.结构 PriorityQueue是一个堆,任意节点都是以它为根节点的子树中的最小节点 堆的逻辑结构是完全二叉树状的,存储结构是用数组去存储的,随机访问性好.最小堆的根元素是最小的,最大堆的根元素是 ...
- Java中堆的实现类PriorityQueue队列接口Queue
Application:这层的职责是对接收到的数据做一些非业务性验证,事务的控制,最重要的是协调多个聚合之间的操作.这里应该可以清晰的表达出整个操作所做的事情,并且与通用语言是一致的. 以上我们讲到可 ...
- Java中的队列Queue,优先级队列PriorityQueue
队列Queue 在java5中新增加了java.util.Queue接口,用以支持队列的常见操作.该接口扩展了java.util.Collection接口. Queue使用时要尽量避免Collecti ...
- PriorityQueue优先队列用法入门
PriorityQueue是队列的一种,它叫做优先队列,该类实现了Queue接口. 之所以叫做优先队列,是因为PriorityQueue实现了Comparator这个比较接口,也就是PriorityQ ...
- 介绍开源的.net通信框架NetworkComms框架 源码分析(十二)PriorityQueue
原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架 作者是英国人 以前是收费的 目前作者已经开源 许可是 ...
- java中PriorityQueue优先级队列使用方法
优先级队列是不同于先进先出队列的另一种队列.每次从队列中取出的是具有最高优先权的元素. PriorityQueue是从JDK1.5开始提供的新的数据结构接口. 如果不提供Comparator的话,优先 ...
随机推荐
- MyBatis拦截器原理探究
MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...
- Java 基础【10】 I/O流概念分析整理
转载地址:http://blog.csdn.net/yuebinghaoyuan/article/details/7388059 java.io 中的流,可以从不同的角度进行分类. 按照数据流的方向不 ...
- 防止 JavaScript 自动插入分号
JavaScript语言有一个机制:在解析时,能够在一句话后面自动插入一个分号,用来修改语句末尾遗漏的分号分隔符. 然而,由于这个自动插入的分号与JavaScript语言的另一个机制发生了冲突,即所有 ...
- SQL2005SP4补丁安装时错误: -2146233087 MSDTC 无法读取配置信息。。。错误代码1603的解决办法
是在安装slq2005sp3和sp4补丁的时候碰到的问题. 起先是碰到的错误1603的问题,但网上搜索的1603的解决办法都试过了,google也用了,外文论坛也读了,依然没有能解决这个问题. 其实一 ...
- WPF 异步加载高清大图
不管什么东西,但凡太大了,总是让人又爱又恨啊!(很有道理的样子,大家鼓掌└( ̄  ̄└)(┘ ̄  ̄)┘) 猿:老板,现在这社会啊,真是浮躁啊,之前还是什么1080P,然后就到了2K,现在又到了4K……他 ...
- ASP.NET Web API 实现客户端Basic(基本)认证 之简单实现
优点是逻辑简单明了.设置简单. 缺点显而易见,即使是BASE64后也是可见的明文,很容易被破解.非法利用,使用HTTPS是一个解决方案. 还有就是HTTP是无状态的,同一客户端每次都需要验证. 实现: ...
- Google 面试
坚持完成这套学习手册,你就可以去 Google 面试了 系统 指针 value Google 面试 阅读10266 本文为掘金投稿,译文出自:掘金翻译计划 原文地址:Google Intervie ...
- shell note
1 输出重定向:ll > aaa 将输出内容 添加到aaa文件中 ll >> aaa将输出内容追加到aaa中 ll &>> abc 将输出内容不论正确或错误都保存 ...
- "此站点已经禁用应用程序"在sharepoint 2013中通过v2013部署app提示该错误
该错误的原文是:the apps are disabled in this site 可以在yahoo或者bing上搜索这个错误,可以找到解决办法: msdn上也有该错误解决办法,但是如果搜索中文,目 ...
- 1025WHERE执行顺序以及MySQL查询优化器
转自http://blog.csdn.net/zhanyan_x/article/details/25294539 -- WHERE执行顺序-- 过滤比较多的放在前面,然后更加容易匹配,从左到右进行执 ...