除特别注明外,本站所有文章均为原创,转载请注明地址

一.优先队列的应用

优先队列在程序开发中屡见不鲜,比如操作系统在进行进程调度时一种可行的算法是使用优先队列,当一个新的进程被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源码剖析的更多相关文章

  1. ArrayDeque(JDK双端队列)源码深度剖析

    ArrayDeque(JDK双端队列)源码深度剖析 前言 在本篇文章当中主要跟大家介绍JDK给我们提供的一种用数组实现的双端队列,在之前的文章LinkedList源码剖析当中我们已经介绍了一种双端队列 ...

  2. 如何查看JDK以及JAVA框架的源码

    如何查看JDK以及JAVA框架的源码 设置步骤如下: 1.点 “window”-> "Preferences" -> "Java" -> &q ...

  3. 如何定位jdk中的native方法源码?

    前提条件:已下载openjdk的源码. 以System类的arraycopy为例: 1. 根据关键字定位文件:grep -rn '"arraycopy"' ./openjdk关键字 ...

  4. 如何阅读jdk及开源框架的源码?

    1.熟悉设计模式 可以边读源码 ,边熟悉设计模式,理解编程思想. jdk中对应的设计模式见:http://blog.csdn.net/gtuu0123/article/details/6114197 ...

  5. spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析

    spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析 TransportContext 首先官方文档对Transpor ...

  6. JDK中String类的源码分析(二)

    1.startsWith(String prefix, int toffset)方法 包括startsWith(*),endsWith(*)方法,都是调用上述一个方法 public boolean s ...

  7. java读源码 之 queue源码分析(PriorityQueue,附图)

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

  8. 【JDK命令行 一】手动编译Java源码与执行字节码命令合集(含外部依赖引用)

    写作目标 记录常见的使用javac手动编译Java源码和java手动执行字节码的命令,一方面用于应对 Maven 和 Gradle 暂时无法使用的情况,临时生成class文件(使用自己的jar包):另 ...

  9. v78.01 鸿蒙内核源码分析(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 | 百篇博客分析OpenHarmony源码

    百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析( ...

随机推荐

  1. 多个Activity交互的生命周期:

    一.多个Activity交互的生命周期: A Activity打开B Activity的时候:        A Activity                    B Activity     ...

  2. Solr DateRangeField

    Solr DateRangeField 是solr5.0 实现的一个feature. 关于 DateRangeField 支持日期查询如  date_field:[2016-03-23 TO 2017 ...

  3. 设置node服务器的端口及运行环境

    一般来说,node服务器中通常会看到这样的代码: ``` app.set('port', process.env.PORT || 3000); if ( process.env.NODE_ENV == ...

  4. linux apache添加多站点配置(Ubuntn和Centos)

     Linux Apache 多站点配置 Centos 配置方式: 找到 /etc/httpd/conf/http.conf 添加监听端口,eg: Listen 89 虚拟机配置,一个端口对应一个 &l ...

  5. Java基础--定时任务Timer(转载)

    Java基础--定时任务Timer 一.Timer介绍 java.util.Timer java.util.TimerTask Timer是一个定时器类,通过该类可以为指定的定时任务进行配置.Time ...

  6. [原创]HBase学习笔记(4)- 数据导入

    需要分别从Oracle和文本文件往HBase中导入数据,这里介绍几种数据导入方案. 1.使用importTSV导入HBase importTSV支持增量导入.新数据插入,已存在数据则修改. 1.1.首 ...

  7. iOS开发之layoutSubviews

    当发生下面两种情况该方法会被调用: (1)一个控件的frame发生改变的时候. (2)布局子控件的时候 一般在这里布局内部的子控件(设置子控件的frame) 例如: - (void)layoutSub ...

  8. 使用Docker容器来源码编译etcd

    背景 etcd是CoreOS公司开发的分布式键值对存储库.在Kubernetes中,我们需要使用etcd作为所有REST API对象的持久化存储. 不幸的是,在github的release中,Core ...

  9. js简单省级联动菜单

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. angular替代Jquery,常用方法支持

    1.angular.bind(self,fn.args);   切换作用域执行 2.angular.copy(source,[destination]);   拷贝和深度拷贝 3.angular.eq ...