在Java SE 5.0中,引入了一些新的Collection API,PriorityQueue就是其中的一个。今天由于机缘巧合,花了一个小时看了一下这个类的内部实现,代码很有点意思,所以写下来跟大家分享一下。从中也可以看到,Java源代码的OpenSource对于我们程序员编程带来了多大的帮助。

最初的起因是我阅读文档不仔细,使用PriorityQueue出现了问题。我刚开始只是把它当作一个一般的FIFO实现来使用,结果发现poll()的结果跟我想象的不一样,后来才发现,PriorityQueue会对入队的元素进行排序,所以在队列顶端的总是最小的元素。

有趣的是,我在仔细阅读文档以前,曾经用调试器察看了我的PriorityQueue,所以即便我后来阅读文档知道了它的正确行为,却发现内部实现似乎跟我想象的不同。把问题简化成下面的代码:

public static void main(String[] args) {
PriorityQueue<String> pq = new PriorityQueue<String>();
pq.add("dog");
pq.add("apple");
pq.add("fox");
pq.add("easy");
pq.add("boy"); while (!pq.isEmpty()) {
for (String s : pq) {
System.out.print(s + " ");
}
System.out.println();
System.out.println("pq.poll(): " + pq.poll());
}
}

输出的结果如下:

apple boy fox easy dog
pq.poll(): apple
boy dog fox easy
pq.poll(): boy
dog easy fox
pq.poll(): dog
easy fox
pq.poll(): easy
fox
pq.poll(): fox

可以看到,虽然PriorityQueue保持了队列顶部元素总是最小,但内部的其它元素的顺序却随着元素的减少始终处于变化之中。由于没有总结出有效的规律,我决定察看源代码来一探究竟。从Netbeans中非常方便的连接到PriorityQueue的add函数实现,最终跟踪到函数private void siftUpComparable(int k, E x),定义如下:

private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}

相对于add的操作,该函数的入口参数k是指新加入元素的下标,而x就是新加入的元素。乍一看,这个函数的实现比较令人费解,尤其是parent的定义。通过进一步分析了解到,PriorityQueue内部成员数组queue其实是实现了一个二叉树的数据结构,这棵二叉树的根节点是queue[0],左子节点是queue[1],右子节点是queue[2],而queue[3]又是queue[1]的左子节点,依此类推,给定元素queue[i],该节点的父节点是queue[(i-1)/2]。因此parent变量就是对应于下标为k的节点的父节点。

弄清楚了这个用数组表示的二叉树,就可以理解上面的代码中while循环进行的工作是,当欲加入的元素小于其父节点时,就将两个节点的位置交换。这个算法保证了如果只执行add操作,那么queue这个二叉树是有序的:该二叉树中的任意一个节点都小于以该节点为根节点的子数中的任意其它节点。这也就保证了queue[0],即队顶元素总是所有元素中最小的。

需要注意的是,这种算法无法保证不同子树上的两个节点之间的大小关系。举例来说,queue[3]一定会小于queue[7],但是未必会小于queue[9],因为queue[9]不在以queue[3]为根节点的子树上。

弄清楚了add的操作,那么当队列中的元素有变化的时候,对应的数组queue又该如何变化呢?察看函数poll(),最终追中到函数private E removeAt(int i),代码如下:

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,替代被删除的元素,然后做相应的调整,保证二叉树的有序,即任意节点都是以它为根节点的子树中的最小节点。进一步的代码就留给有兴趣的读者自行分析,要说明的是,对于queue这样的二叉树结构有一个特性,即如果数组的长度为length,那么所有下标大于length/2的节点都是叶子节点,其它的节点都有子节点。

总结:可以看到这种算法的实现,充分利用了树结构在搜索操作时的优势,效率又高于维护一个全部有序的队列。

补充:

堆排序只能保证根是最大(最小),不能保证整体是按照顺序来排序的。比如

一开始程序的加载:

1. boy

2. apple

dog

3. apple

dog     fox

4. apple

dog  fox

easy

5. apple

boy  fox

easy dog

只是保证新加入的元素和最近的父节点是排序的。

PriorityQueue是个基于优先级堆的极大优先级队列。

此队列按照在构造时所指定的顺序对元素排序,既可以根据元素的自然顺序来指定排序(参阅 Comparable),
也可以根据 Comparator 来指定,这取决于使用哪种构造方法。优先级队列不允许 null 元素。 依靠自然排序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。
此队列的头是按指定排序方式的最小元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。 队列检索操作 poll、remove、peek 和 element 访问处于队列头的元素。
优先级队列是无界的,但是有一个内部容量,控制着用于存储队列元素的数组的大小。
它总是至少与队列的大小相同。随着不断向优先级队列添加元素,其容量会自动增加。无需指定容量增加策略的细节。

注意1:

该队列是用数组实现,但是数组大小可以动态增加,容量无限。

注意2:

此实现不是同步的。不是线程安全的。如果多个线程中的任意线程从结构上修改了列表, 则这些线程不应同时访问 PriorityQueue 实例,这时请使用线程安全的PriorityBlockingQueue 类。

注意3:

不允许使用 null 元素。

注意4:

此实现为插入方法(offer、poll、remove() 和 add 方法)提供 O(log(n)) 时间;
为 remove(Object) 和 contains(Object) 方法提供线性时间;
为检索方法(peek、element 和 size)提供固定时间。

注意5:

方法iterator()中提供的迭代器并不保证以有序的方式遍历优先级队列中的元素。
至于原因可参考下面关于PriorityQueue的内部实现
如果需要按顺序遍历,请考虑使用 Arrays.sort(pq.toArray())。

注意6:

可以在构造函数中指定如何排序。

如:
PriorityQueue()

使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用 Comparable)。

PriorityQueue(int initialCapacity)
使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序来排序其元素(使用 Comparable)。
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
使用指定的初始容量创建一个 PriorityQueue,并根据指定的比较器comparator来排序其元素。

注意7:此类及其迭代器实现了 Collection 和 Iterator 接口的所有可选 方法。
PriorityQueue的内部实现
PriorityQueue对元素采用的是堆排序,头是按指定排序方式的最小元素。堆排序只能保证根是最大(最小),整个堆并不是有序的。
方法iterator()中提供的迭代器可能只是对整个数组的依次遍历。也就只能保证数组的第一个元素是最小的。

PriorityQueue详解(一)的更多相关文章

  1. PriorityQueue详解

    美人如斯!好好看文章! 前言 java中关于Queue队列的实现繁多(关于Queue可以移步至我的另一篇文章:<Queue介绍>),每种实现根据自身的特性都有相应的应用场景.这里我们就来聊 ...

  2. 【Java入门提高篇】Day33 Java容器类详解(十五)PriorityQueue详解

    今天要介绍的是基础容器类(为了与并发容器类区分开来而命名的名字)中的另一个成员——PriorityQueue,它的大名叫做优先级队列,想必即使没有用过也该有所耳闻吧,什么?没..没听过?emmm... ...

  3. Java中PriorityQueue详解

    Java中PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示.本文从Queue接口函数出发,结合生动的图解,深入浅出地分析PriorityQueue每个操作的具体过程和时间复杂度, ...

  4. Java的优先队列PriorityQueue详解

    一.优先队列概述 优先队列PriorityQueue是Queue接口的实现,可以对其中元素进行排序, 可以放基本数据类型的包装类(如:Integer,Long等)或自定义的类 对于基本数据类型的包装器 ...

  5. Heapsort 堆排序算法详解(Java实现)

    Heapsort (堆排序)是最经典的排序算法之一,在google或者百度中搜一下可以搜到很多非常详细的解析.同样好的排序算法还有quicksort(快速排序)和merge sort(归并排序),选择 ...

  6. ScheduledThreadPoolExecutor详解

    本文主要分为两个部分,第一部分首先会对ScheduledThreadPoolExecutor进行简单的介绍,并且会介绍其主要API的使用方式,然后介绍了其使用时的注意点,第二部分则主要对Schedul ...

  7. Java集合中List,Set以及Map等集合体系详解

    转载请注明出处:Java集合中List,Set以及Map等集合体系详解(史上最全) 概述: List , Set, Map都是接口,前两个继承至collection接口,Map为独立接口 Set下有H ...

  8. Python中的高级数据结构详解

    这篇文章主要介绍了Python中的高级数据结构详解,本文讲解了Collection.Array.Heapq.Bisect.Weakref.Copy以及Pprint这些数据结构的用法,需要的朋友可以参考 ...

  9. STL之heap与优先级队列Priority Queue详解

    一.heap heap并不属于STL容器组件,它分为 max heap 和min heap,在缺省情况下,max-heap是优先队列(priority queue)的底层实现机制.而这个实现机制中的m ...

随机推荐

  1. 嵌入式Linux编译内核步骤 / 重点解决机器码问题 / 三星2451

    嵌入式系统更新内核 1. 前言 手里有一块Friendly ARM的MINI2451的板子,这周试着编译内核,然后更新一下这个板子的Linux内核,想要更新Linux Kernel 4.1版本,但是种 ...

  2. 636. Exclusive Time of Functions

    // TODO: need improve!!! class Log { public: int id; bool start; int timestamp; int comp; // compasa ...

  3. 1014-34-首页15-计算原创微博的frame------计算cell的高度---计算 UILabel 的 CGSize 的方法

    一.总体思路: 在控制器中,每次拿到数据模型(请求了数据.加载新微博)的时候,就调用 - (NSArray *)stausFramesWithStatuses:(NSArray *)statuses, ...

  4. Educational Codeforces Round 37 E. Connected Components?(图论)

    E. Connected Components? time limit per test 2 seconds memory limit per test 256 megabytes input sta ...

  5. python向多个邮箱发邮件--注意接收是垃圾邮件

    群发邮件注意:三处标红的地方 # -*- coding: UTF-8 -*- import smtplib from email.mime.text import MIMEText from emai ...

  6. tar命令,vi编辑器

    一.将用户信息数据库文件和组信息数据库文件纵向合并为一个文件/1.txt(覆盖): [root@localhost /]# cat /etc/passwd /etc/group > 1.txt ...

  7. CodeForces 879D Teams Formation

    题意 将一个长度为\(n\)的数组重复\(m\)遍得到一个长度为\(n \times m\)的新序列,然后消掉新序列中连续\(k\)个相同的元素,不断重复这一过程,求最后剩下的序列的长度 分析 首先可 ...

  8. 关于android 5.0报错:dlopen failed: couldn't map ... Permission denied

    问题描述: 我的应用当中集成了一个安全相关的sdk,而这个sdk中使用的so是加过壳的. 它加载native so的方式是:Java System.loadLibrary --> native ...

  9. spring整合redis缓存,以注解(@Cacheable、@CachePut、@CacheEvict)形式使用

    maven项目中在pom.xml中依赖2个jar包,其他的spring的jar包省略: <dependency> <groupId>redis.clients</grou ...

  10. APP遇到大量的真实手机号刷注册用户该如何应对?

    欢迎访问网易云社区,了解更多网易技术产品运营经验. 在说如何应对之前,先给各位梳理移动端APP可能遇到哪些作弊风险.1. 渠道商刷量,伪造大量的下载量和装机量,但没有新用户注册:2. 对于电商.P2P ...