JDK源码之PriorityQueue源码剖析
除特别注明外,本站所有文章均为原创,转载请注明地址
一.优先队列的应用
优先队列在程序开发中屡见不鲜,比如操作系统在进行进程调度时一种可行的算法是使用优先队列,当一个新的进程被fork()出来后,首先将它放到队列的最后,而操作系统内部的Scheduler负责不断地从这个优先队列中取出优先级较高的进程执行;爬虫系统在执行时往往也需要从一个优先级队列中循环取出高优先级任务并进行抓取。可以想见,如果类似这样的任务不适用优先级进行划分的话,系统必会出现故障,例如操作系统中低优先级进程持续占用资源而高优先级进程始终在队列中等待。此外,优先队列在贪婪算法中也有一些应用。
二.优先队列的实现原理
优先队列的实现方式是使用二叉堆的结构,需要满足以下两条性质(Heap property),这里以小顶堆为例讲解:
1.任何结点的值都小于或等于其子节点的值。
2.所有结点从上到下,从左到右填入,即一棵完全二叉树。
基于这两条规律,二叉堆在实现中往往会使用一个数组,下面我们研究一下JDK中二叉堆(优先队列)的实现。
三.优先队列在JDK中的实现方式
研究源码最好的方式是debug,看每一步变量的变化,我们可以简单写一个Demo,debug进源码一探究竟:
这里我们简单地创建一个优先队列,向其中添加三个元素,我们可以在代码第一行打一个断点,如果您使用Eclipse编辑器的话,接下来可以按F5进入源码中:
代码运行到这里,PriorityQueue调用自己的一个重载构造器,第一个参数是数组默认大小,第二个是元素比较的Comparator,我们这里的Demo比较简单,
您在使用优先队列时可以选择实现自己的Comparator。
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}
接下来我们研究一下添加元素时的offer操作:
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)
//第一次添加元素,直接放到第0个位置即可
queue[0] = e;
else
//需要将元素放到最后,再做上滤操作
siftUp(i, e);
return true;
}
我们逐行来解释一下,首先offer方法判断参数是否为空,不为空则对变量modCount自增,
modCount记录了队列被修改的次数,接下来,判断数组是否会越界,如果越界则通过grow进行扩容,
接下来添加元素,如果当前元素为0个则直接将元素放到数组第一个位置,否则做一个siftUp的操作。
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);
上面的代码对队列扩容,源码中注释也很清晰,首先判断当前的数组大小是否足够小(<64)
如果足够小则将大小扩充为2倍,否则将原大小加上50%。需要注意的是,这里最后做了
一个大小是否溢出的判断。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
如果需要扩容的大小已经<0了,显然已经溢出了,在这里抛出了OutOfMemory的异常。
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;
}
为了保证优先队列的性质1,在插入每个元素时都需要与该节点父亲进行比较,找到其正确位置,
有些数据结构书中,这个操作被称为上滤(percolate up)。
入队操作已经说完了,接下来是出队操作,即poll()操作:
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;
}
这个方法首先将数组第一个元素作为结果,(因为如果是小顶堆的话堆顶始终是最小元素),并将队列的最后一个元素放到第一个位置,最后用
siftDown做一些调整,保证队列的性质,这个操作被称为下滤(percolate down)。
private void siftDownUsingComparator(int k, E x) { int half = size >>> 1;
//这里k必须有孩子,故叶节点需要比较
while (k < half) {
//以下几行代码到较小的那个儿子,用变量c表示
int child = (k << 1) + 1;
//这里假设左儿子比较小
Object c = queue[child];
int right = child + 1;
//左右儿子比较,如果右儿子小则将c赋值为右儿子
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
//如果x比小儿子还小,说明k就是正确位置
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
如上图,下滤过程中k不断与其儿子进行比较,如果满足优先队列的顺序性,则break出循环。
JDK源码之PriorityQueue源码剖析的更多相关文章
- ArrayDeque(JDK双端队列)源码深度剖析
ArrayDeque(JDK双端队列)源码深度剖析 前言 在本篇文章当中主要跟大家介绍JDK给我们提供的一种用数组实现的双端队列,在之前的文章LinkedList源码剖析当中我们已经介绍了一种双端队列 ...
- 如何查看JDK以及JAVA框架的源码
如何查看JDK以及JAVA框架的源码 设置步骤如下: 1.点 “window”-> "Preferences" -> "Java" -> &q ...
- 如何定位jdk中的native方法源码?
前提条件:已下载openjdk的源码. 以System类的arraycopy为例: 1. 根据关键字定位文件:grep -rn '"arraycopy"' ./openjdk关键字 ...
- 如何阅读jdk及开源框架的源码?
1.熟悉设计模式 可以边读源码 ,边熟悉设计模式,理解编程思想. jdk中对应的设计模式见:http://blog.csdn.net/gtuu0123/article/details/6114197 ...
- spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析
spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析 TransportContext 首先官方文档对Transpor ...
- JDK中String类的源码分析(二)
1.startsWith(String prefix, int toffset)方法 包括startsWith(*),endsWith(*)方法,都是调用上述一个方法 public boolean s ...
- java读源码 之 queue源码分析(PriorityQueue,附图)
今天要介绍的是基础容器类(为了与并发容器类区分开来而命名的名字)中的另一个成员--PriorityQueue,它的大名叫做优先级队列,想必即使没有用过也该有所耳闻吧,什么?没..没听过?emmm... ...
- 【JDK命令行 一】手动编译Java源码与执行字节码命令合集(含外部依赖引用)
写作目标 记录常见的使用javac手动编译Java源码和java手动执行字节码的命令,一方面用于应对 Maven 和 Gradle 暂时无法使用的情况,临时生成class文件(使用自己的jar包):另 ...
- v78.01 鸿蒙内核源码分析(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 | 百篇博客分析OpenHarmony源码
百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析( ...
随机推荐
- Pascal's Triangle leetcode
Given numRows, generate the first numRows of Pascal's triangle. For example, given numRows = 5,Retur ...
- 1054: [HAOI2008]移动玩具
1054: [HAOI2008]移动玩具 Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 1272 Solved: 690[Submit][Statu ...
- ReactJS React+Redux+Router+antDesign通用高效率开发模板,夜间模式为例
工作比较忙,一直没有时间总结下最近学习的一些东西,为了方便前端开发,我使用React+Redux+Router+antDesign总结了一个通用的模板,这个技术栈在前端开发者中是非常常见的. 总的来说 ...
- Microsoft Visual Studio 2017 安装过程
工欲善其事必先利其器 Visual Studio 2017 正式版官方下载地址:https://www.visualstudio.com/downloads/ 安装vs2017的时候最好关闭已打开的v ...
- 【转】JavaScript 之arguments、caller 和 callee 介绍
1.前言 arguments, caller , callee 是什么? 在JavaScript 中有什么样的作用?本篇会对于此做一些基本介绍. 本文转载自:http://blog.csdn.ne ...
- v3学院带你学习EEPROM读写实验
一.实验背景在消费者电子电讯和工业电子中看上去不相关的设计里经常有很多相似的地方例如几乎每个系统都包括一些智能控制通常是一个单片的微控制器,通用电路例如LCD驱动器远程I/O,RAM,EEPROM或数 ...
- iOSiOS开发之数据存储之NSKeyedArchiver
1.概述 NSKeyedArchiver归档和plist文件存储不同的是NSKeyedArchiver可以直接保存对象.如果对象是NSString.NSDictionary.NSArray.NSDat ...
- python如何保证输入键入数字
要求:python写一个要求用户输入数字,如果不是数字就一直循环要求输入,直到输入数字为止的代码 错误打开方式: while True: ten=input('Enter a number:') if ...
- Tcl与Design Compiler (十三)——Design Compliler中常用到的命令(示例)总结
本文如果有错,欢迎留言更正:此外,转载请标明出处 http://www.cnblogs.com/IClearner/ ,作者:IC_learner 本文将描述在Design Compliler中常用 ...
- 自适应滤波:奇异值分解SVD
作者:桂. 时间:2017-04-03 19:41:26 链接:http://www.cnblogs.com/xingshansi/p/6661230.html 声明:欢迎被转载,不过记得注明出处哦 ...