作者:炸鸡可乐

原文出处:www.pzblog.cn

一、摘要

在前几篇文章中,咱们了解到,Queue 的实现类有 ArrayDeque、LinkedList、PriorityQueue。

在上一章节中,陆续的介绍到 ArrayDeque 和 LinkedList 的数据结构和算法实现,今天咱们来介绍一下** PriorityQueue 这个类,一个特殊的优先级队列**。如果有理解不当之处,欢迎指正。

二、简介

PriorityQueue 并没有直接实现 Queue接口,而是通过继承 AbstractQueue 类来实现 Queue 接口一些方法,在 Java 定义中,PriorityQueue 是一个基于优先级的无界优先队列。

通俗的说,添加到 PriorityQueue 队列里面的元素都经过了排序处理,默认按照自然顺序,也可以通过 Comparator 接口进行自定义排序。

优先队列的作用是保证每次取出的元素都是队列中权值最小的。

如果猿友们了解过 TreeMap 的实现,会发现 PriorityQueue 排序实现与之类似。

PriorityQueue 是采用树形结构来描述元素的存储,具体说是通过完全二叉树实现一个小顶堆,在物理存储方面,PriorityQueue 底层通过数组来实现元素的存储。

在上图中,我们给每个元素的下标做了标注,足够细心的你会发现,数组下标,存在以下关系:

leftNo = parentNo * 2 + 1
rightNo = parentNo * 2 + 2
parentNo = (currentNo -1) / 2

各个参数具体含义如下:

  • parentNo:表示父节点下标;
  • leftNo:表示子元素左节点下标;
  • rightNo:表示子元素右节点下标;
  • currentNo:表示当前元素节点下标;

通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储元素实现二叉树结构的原因。

2.1、源码介绍

PriorityQueue 源码定义如下:

public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable { /**默认容量为11*/
private static final int DEFAULT_INITIAL_CAPACITY = 11; /**队列容器*/
transient Object[] queue; /**队列长度*/
private int size = 0; /**比较器,为null使用自然排序*/
private final Comparator<? super E> comparator; ......
}

从定义中可以得出,PriorityQueue 有3个比较核心的变量属性,内容如下:

  • queue:表示存放元素的数组
  • comparator:表示比较器对象,如果为空,使用自然排序
  • size:表示队列长度

我们再来看看 PriorityQueue 类的构造方法,PriorityQueue 构造方法分两类,一种是默认初始化、另一种是传入 Comparator 接口比较器,内容如下:

默认初始化,使用自然排序方式进行插入,源码如下:

public PriorityQueue() {
//默认数组长度为11,传入比较器为null
this(DEFAULT_INITIAL_CAPACITY, null);
}

调用的方法,源码如下:

public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
//初始化容量小于 1,抛异常
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
this.comparator = comparator;
}

自定义比较器初始化,使用 comparator 接口比较器作为参数传入,源码如下:

public PriorityQueue(Comparator<? super E> comparator) {
//传入比较器 comparator
this(DEFAULT_INITIAL_CAPACITY, comparator);
}

这两者初始化方式,咱们在下文会一一讲到。

在介绍 PriorityQueue 实现的方法之前,咱们了解到,Queue 接口定义有如下方法:

同样的 PriorityQueue 也实现了这些方法,PriorityQueue 方法虽然定义的很多,但无非就是对容器进行添加、删除、查询操作,下面我们分别来看看各个操作方法的实现过程。

三、常见方法介绍

3.1、添加方法

PriorityQueue 的添加方法有 2 种,分别是add(E e)offer(E e),两者语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则返回false

3.1.1、offer 方法

offer 方法图解实现流程如下:

新加入的元素可能会破坏小顶堆的性质,在 c、d 两步会进行调整。

offer 方法的实现,源码如下:

public boolean offer(E e) {
//不允许放入null元素
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;
}

值得注意的是,插入元素不能为null,否则报空指针异常!

当数组空间不足时,会进行扩容,扩容函数grow()类似于ArrayList里的grow()函数,就是再申请一个更大的数组,并将原数组的元素复制过去,源码如下:

private void grow(int minCapacity) {
int oldCapacity = queue.length;
//如果旧数组容量小于64,新容量为 oldCapacity *2 +2
//如果大于64,新容量为 oldCapacity + oldCapacity * 0.5
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
//判断是否超过最大容量值,设置最高容量值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//复制数组元素
queue = Arrays.copyOf(queue, newCapacity);
}

从源码中可以看出,在计算新容量的时候,如果旧数组的容量小于64,新数组容量为旧容量的2➕2;反之,新数组容量的扩容系数为50%

我们再来看看siftUp(i, e)这个方法,当插入的元素不是顶部位置,会进行内容排序调整,siftUp(i, e)方法就是起到这个作用,源码如下:

private void siftUp(int k, E x) {
//如果使用比较器,采用比较器进行比较
if (comparator != null)
siftUpUsingComparator(k, x);
else
//没有比较器,采用自然排序
siftUpComparable(k, x);
}

默认调整方式的实现,源码如下:

private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
//parentNo = (nodeNo-1)/2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
//默认自然排序,从小到大
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}

自定义比较器的实现,调整方式,源码如下:

private void siftUpUsingComparator(int k, E x) {
while (k > 0) {
//parentNo = (nodeNo-1)/2
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;
}

默认的插入规则中,新加入的元素可能会破坏小顶堆的性质,因此需要进行调整。

调整的过程为:从尾部下标的位置开始,将加入的元素逐层与当前点的父节点的内容进行比较并交换,直到满足父节点内容都小于子节点的内容为止。

当然,也可以依靠自定义比较器,实现自定排序规则。

3.1.2、add 方法

add方法,就比较简单了,直接调用了offer方法,返回false抛异常,源码如下:

public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
3.1.3 使用方式
  • 自然排序
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
System.out.println("插入的数据");
//随机添加两位数
for (int i = 0; i < 10; i++) {
Integer num = new Random().nextInt(90) + 10;
System.out.print(num + ",");
queue.offer(num);
} System.out.println("\n输出后的数据");
while (true){
Integer result = queue.poll();
if(result == null){
break;
}
System.out.print(result + ",");
}
}

输出结果:

插入的数据
53,97,66,58,69,10,72,27,18,16,
输出后的数据
10,16,18,27,53,58,66,69,72,97,
  • 自定义排序
public static void main(String[] args) {
PriorityQueue<Integer> customeQueue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//按照大到小排序
return o2.compareTo(o1);
}
});
System.out.println("插入的数据");
//随机添加两位数
for (int i = 0; i < 10; i++) {
Integer num = new Random().nextInt(90) + 10;
System.out.print(num + ",");
customeQueue.offer(num);
} System.out.println("\n输出后的数据");
while (true){
Integer result = customeQueue.poll();
if(result == null){
break;
}
System.out.print(result + ",");
}
}

输出结果:

插入的数据
66,39,28,54,56,66,54,77,10,97,
输出后的数据
97,77,66,66,56,54,54,39,28,10,

3.2、删除方法

PriorityQueue 的删除方法有 2 种,分别是remove()poll(),两者语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。

3.2.1、poll 方法

offer 方法图解实现流程如下:

删除的元素可能会破坏小顶堆的性质,在 b、 c、d 三步会进行调整。

poll 方法的实现,源码如下:

public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
//0下标处的那个元素就是最小的那个
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
//调整
siftDown(0, x);
return result;
}

调整过程与插入的调整过程有些相反!

首先记录数组头部的下标,并用最后一个元素的内容替换数组头部的元素,之后调用siftDown()方法对堆进行调整,最后返回数组头部的元素。

siftDown(int k, E x)方法的实现,源码内容如下:

private void siftDown(int k, E x) {
//判断是否有自定义比较器
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}

与插入的调整类似,首先判断是否有自定义的比较器,如果没有,按照默认的方式进行调整,反之,根据自定义比较器的排序规则进行调整。

默认调整方式,函数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) {
//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
int child = (k << 1) + 1;
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;//然后用c取代原来的值
k = child;
}
queue[k] = key;
}

自定义调整方式,函数siftDownUsingComparator(k, x),源码如下:

private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;//然后用c取代原来的值
k = child;
}
queue[k] = x;
}

默认的删除调整中,首先获取顶部下标和最尾部的元素内容,从顶部的位置开始,将尾部元素的内容逐层向下与当前点的左右子节点中较小的那个交换,直到判断元素内容小于或等于左右子节点中的任何一个为止。

如果有自定义比较器,使用自定义比较器中的排序算法来进行交换。

思路是一样的,只是排序比较算法不一样而已!

3.2.2、remove 方法

remove 方法实现比较简单,直接调用了poll()方法,返回空值抛异常,源码如下:

public E remove() {
E x = poll();
if (x != null)
return x;
else
//返回空值,抛异常
throw new NoSuchElementException();
}

3.3、查询方法

PriorityQueue 的查询方法有 2 种,分别是element()和peek(),两者语义也完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null

因为是数组结构,所以查询的时间复杂度log(1),根据小顶堆的性质,堆顶那个元素就是全局最小的那个,直接返回数组下标为0即可返回队首元素!

3.3.1、peek 方法

peek 方法图解实现流程如下:

peek 方法实现,直接返回数组下标为0的元素,源码如下:

public E peek() {
return (size == 0) ? null : (E) queue[0];
}
3.3.2、element 方法

element 方法实现也比较简单,直接调用了peek()方法,如果返回空值抛异常,源码如下:

public E element() {
E x = peek();
if (x != null)
return x;
else
//返回空值,抛异常
throw new NoSuchElementException();
}

四、总结

在 Java 中 PriorityQueue 是一个使用数组结构来存储元素的优先队列,虽然它也实现了Queue接口,但是元素存取并不是先进先出,而是通过一个二叉小顶堆实现的,默认底层使用自然排序规则给插入的元素进行排序,也可以使用自定义比较器来实现排序,每次取出的元素都是队列中权值最小的。

同时需要注意的是,PriorityQueue 不能插入null,否则报空指针异常!

五、参考

1、JDK1.7&JDK1.8 源码

2、知乎 - CarpenterLee -深入理解Java PriorityQueue

深入浅出分析 PriorityQueue的更多相关文章

  1. 深入浅出分析C#接口的作用

    1.C#接口的作用 :C#接口是一个让很多初学C#者容易迷糊的东西,用起来好像很简单,定义接口,里面包含方法,但没有方法具体实现的代码,然后在继承该接口的类里面要实现接口的所有方法的代码,但没有真正认 ...

  2. 【集合系列】- 深入浅出分析 ArrayDeque

    一.摘要 在 jdk1.5 中,新增了 Queue 接口,代表一种队列集合的实现,咱们继续来聊聊 java 集合体系中的 Queue 接口. Queue 接口是由大名鼎鼎的 Doug Lea 创建,中 ...

  3. Android指纹识别深入浅出分析到实战(6.0以下系统适配方案)

    指纹识别这个名词听起来并不陌生,但是实际开发过程中用得并不多.Google从Android6.0(api23)开始才提供标准指纹识别支持,并对外提供指纹识别相关的接口.本文除了能适配6.0及以上系统, ...

  4. jdk源码分析PriorityQueue

    一.结构 PriorityQueue是一个堆,任意节点都是以它为根节点的子树中的最小节点 堆的逻辑结构是完全二叉树状的,存储结构是用数组去存储的,随机访问性好.最小堆的根元素是最小的,最大堆的根元素是 ...

  5. 深入浅出分析MySQL MyISAM与INNODB索引原理、优缺点、主程面试常问问题详解

    本文浅显的分析了MySQL索引的原理及针对主程面试的一些问题,对各种资料进行了分析总结,分享给大家,希望祝大家早上走上属于自己的"成金之路". 学习知识最好的方式是带着问题去研究所 ...

  6. 深入浅出分析MySQL索引设计背后的数据结构

    在我们公司的DB规范中,明确规定: 1.建表语句必须明确指定主键 2.无特殊情况,主键必须单调递增 对于这项规定,很多研发小伙伴不理解.本文就来深入简出地分析MySQL索引设计背后的数据结构和算法,从 ...

  7. 深入浅出分析MySQL MyISAM与INNODB索引原理、优缺点分析

    本文浅显的分析了MySQL索引的原理及针对主程面试的一些问题,对各种资料进行了分析总结,分享给大家,希望祝大家早上走上属于自己的"成金之路". 学习知识最好的方式是带着问题去研究所 ...

  8. Java容器深入浅出之PriorityQueue、ArrayDeque和LinkedList

    Queue用于模拟一种FIFO(first in first out)的队列结构.一般来说,典型的队列结构不允许随机访问队列中的元素.队列包含的方法为: 1. 入队 void add(Object o ...

  9. 深入浅出分析MySQL常用存储引擎

    MyISAM是MySQL的默认数据库引擎(5.5版之前),由早期的ISAM(Indexed Sequential Access Method:有索引的顺序访问方法)所改良.虽然性能极佳,但却有一个缺点 ...

随机推荐

  1. opencv 6 图像轮廓与图像分割修复 3 图像的矩,分水岭,图像修补

    图像的矩 矩的计算:moments()函数 计算轮廓面积:contourArea()函数 #include "opencv2/highgui/highgui.hpp" #inclu ...

  2. beta week 1/2 Scrum立会报告+燃尽图 02

    此作业要求参见https://edu.cnblogs.com/campus/nenu/2019fall/homework/9912 一.小组情况 队名:扛把子 组长:孙晓宇 组员:宋晓丽 梁梦瑶 韩昊 ...

  3. 新闻实时分析系统 SQL快速离线数据分析

    1.Spark SQL概述1)Spark SQL是Spark核心功能的一部分,是在2014年4月份Spark1.0版本时发布的. 2)Spark SQL可以直接运行SQL或者HiveQL语句 3)BI ...

  4. 新闻实时分析系统-Kafka分布式集群部署

    Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用.目前越来越多的开源分布式处理系统如Cloudera.Apache Storm.Spa ...

  5. 小白学习python第一天,Pycharm破解与用法(持续更新)

    目录 Pycharm安装与破解及汉化 Pycharm安装 Pycharm破解 Pycharm汉化 Pycharm使用 添加作者.时间等信息 补充 @ Pycharm安装与破解及汉化 本人最近开始找到了 ...

  6. Kafka原理详解

    Kafka是最初由Linkedin公司开发,是一个分布式.支持分区的(partition).多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量 ...

  7. 虚拟机配置net模式

    在cmd中输入ipconfig -all查看 更改网络适配器 进入虚拟机左上角编辑----虚拟机网络编辑器查看VMnet8,虚拟机会为我们分配的固定ip段:如下图: ip段是128---254,所以设 ...

  8. Servlet+Ajax实现搜索框智能提示

    简介:搜索框相信大家都不陌生,几乎每天都会在各类网站进行着搜索.有没有注意到,很多的搜索功能,当输入内容时,下面会出现提示.这类提示就叫做搜索框的智能提示,本门课程就为大家介绍如何使用Servlet和 ...

  9. spring源码学习五 - xml格式配置,如何解析

    spring在注入bean的时候,可以通过bean.xml来配置,在xml文件中配置bean的属性,然后spring在refresh的时候,会去解析xml配置文件,这篇笔记,主要来记录.xml配置文件 ...

  10. python_regex

    正则表达动机(目的):    1.处理文本成为计算机主要工作之一    2.根据文本内容进行固定搜索是文本处理的常见工作    3.为了快速方便的处理上述问题,正则表达式技术诞生,逐渐发展为一种单独技 ...