集合系列(十二) -PriorityQueue详解

2024-03-21 11:28

本文主要是介绍集合系列(十二) -PriorityQueue详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、摘要

在前几篇文章中,咱们了解到,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,传入比较器为nullthis(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) {//传入比较器 comparatorthis(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.5int 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)/2int 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)/2int 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;elsethrow 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>() {@Overridepublic 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);elsesiftDownComparable(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-leafwhile (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

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

feda2a37-72b7-4bb7-82a8-b91660cd1b89

这篇关于集合系列(十二) -PriorityQueue详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832536

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

uva 11178 计算集合模板题

题意: 求三角形行三个角三等分点射线交出的内三角形坐标。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <stack>#include <vector>#include <

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor