Java编程的逻辑 (46) - 剖析PriorityQueue
本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html
上节介绍了堆的基本概念和算法,本节我们来探讨堆在Java中的具体实现类 - 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)。
小结
本节介绍了Java中堆的实现类PriorityQueue,它实现了队列接口Queue,但按优先级出队,我们介绍了其用法和实现代码。
除了用作基本的优先级队列,PriorityQueue还可以作为一种比较通用的数据结构,用于解决一些其他问题,让我们在下一节继续探讨。
---------------
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。
Java编程的逻辑 (46) - 剖析PriorityQueue的更多相关文章
- Java编程的逻辑 (48) - 剖析ArrayDeque
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- 计算机程序的思维逻辑 (46) - 剖析PriorityQueue
上节介绍了堆的基本概念和算法,本节我们来探讨堆在Java中的具体实现类 - PriorityQueue. 我们先从基本概念谈起,然后介绍其用法,接着分析实现代码,最后总结分析其特点. 基本概念 顾名思 ...
- Java编程的逻辑 (51) - 剖析EnumSet
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (26) - 剖析包装类 (上)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (27) - 剖析包装类 (中)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (28) - 剖析包装类 (下)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (32) - 剖析日期和时间
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (53) - 剖析Collections - 算法
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (30) - 剖析StringBuilder
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
随机推荐
- PHP中的$_SERVER超全局变量
详细参数 PHP编程中经常需要用到一些服务器的一些资料,特把$_SERVER的详细参数整理下,方便以后使用. $_SERVER['PHP_SELF'] #当前正在执行脚本的文件名,与 document ...
- jeecg中的一个上下文工具类获取request,session
通过调用其中的方法可以获取到request和session,调用方式如下: HttpServletRequest request = ContextHolderUtils.getRequest();H ...
- mysql group replication 主节点宕机恢复
一.mysql group replication 生来就要面对两个问题: 一.主节点宕机如何恢复. 二.多数节点离线的情况下.余下节点如何继续承载业务. 在这里我们只讨论第一个问题.也就是说当主结点 ...
- 解决ssh连接慢(有时候等半分钟才出现密码输入提示)的方法
经常通过ssh 或者 scp 连接一堆远程主机,同样是 Linux 主机,其中一些创建 ssh 连接速度特别慢,连接建立之后执行操作速度却很正常,看来应该不是网络原因.解决的方法是通过ssh 的-v参 ...
- OpenCV Machine Learning 之 K近期邻分类器的应用 K-Nearest Neighbors
OpenCV Machine Learning 之 K近期邻分类器的应用 以下的程序实现了对高斯分布的点集合进行分类的K近期令分类器 #include "ml.h" #includ ...
- [svc]centos6使用chkconfig治理服务和其原理
centos6开机启动级别 $ cat /etc/inittab ... # 0 - halt (Do NOT set initdefault to this) # 1 - Single user m ...
- su 和 su -
单纯使用su切揣到root,读取变量的方式 是non-login shell,这种方式下很多的变量都 不会改变,尤其是PATH,所以root用的很多命令都只能用绝对路径来执行,这种方式只是切换到roo ...
- android studio - Manifest merger failed with multiple errors, see logs
今天编译运行的时候遇到了“Error:Execution failed for task ':test:processDebugManifest'.> Manifest merger faile ...
- Berkeley Packet Filter (BPF) BCC
http://www.brendangregg.com/ebpf.html https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-b ...
- Zen Coding)官方文档 一览表
语法 Child: > nav>ul>li <nav> <ul> <li></li> </ul> </nav> ...