Java数据结构(七)——优先级队列与PriorityQueue

2024-09-01 01:52

本文主要是介绍Java数据结构(七)——优先级队列与PriorityQueue,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 优先级队列与PriorityQueue
      • 基本概念和性质
      • 建堆
      • 堆的插入
      • 堆的删除
      • 堆的应用
    • PriorityQueue
      • PriorityQueue的构造方法
      • PriorityQueue的常用方法
      • PriorityQueue的模拟实现
    • 经典TopK问题

优先级队列与PriorityQueue

优先级队列是一种特殊类型的队列,其中元素按照优先级进行排序,最高优先级的元素最先被取出。其概念源于对元素进行优先级排序的需求。与普通队列先进先出(FIFO)的原则不同,优先级队列中的元素根据其优先级值进行排列和取出。在许多场景下,如任务调度、交通信号管理等,优先级队列都发挥着重要作用。

Java中的优先级队列(PriorityQueue)是一种利用 数据结构实现的、按照元素优先级排序的队列,它允许元素以任意顺序插入,但取出时会按照优先级高低进行排序。

基本概念和性质

是一种特殊的完全二叉树,其中每个节点的值总是大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。

准确来说,堆在逻辑上是一棵完全二叉树,物理结构上常用数组表示,存储时按照层序遍历的次序存入数组。
所以,我们先要掌握堆的相关知识。

堆的两个特性:

  1. 结构性: 用数组表示一棵完全二叉树
  2. 有序性: 任一结点的关键字是其子树所有节点的最大值(或最小值)。
    “最大堆”,也称“大顶堆”(或“大根堆”),简称大堆。特点是:父亲大于等于孩子
    “最小堆”,也称“小顶堆”(或“小根堆”),简称小堆。特点是:父亲小于等于孩子

在这里插入图片描述

观察图片,最大堆中第二层的7小于第三层的9和11,因为堆只要求父结点大于(或小于)自己的子结点,而不要求大于(或小于)其同层结点的子结点。不要认为第k层的结点都大于(小于)第k+1层!


由于堆的存储方式是数组以及堆性质的特殊性,有以下公式(parent等均表示数组下标,以第一个公式为例,其含义:父结点的下标 = (任一孩子下标 - 1) / 2):

  • parent = (child - 1) / 2
  • leftchild = parent * 2 + 1
  • rightchild = parent * 2 + 2

第一个公式结果小于0时,代表没有父结点;当第二、三个公式的结果大于数组最后一个元素的下标时,代表没有孩子结点。

练习:

下列关键字序列为堆的是()

A. 60,70,65,50,32,100 B. 100,60,70,50,32,65 C. 32,50,100,70,65,60 D. 70,65,100,32,50,60

解: 由于堆逻辑上是一棵完全二叉树,存储时按照层序遍历顺序,所以直接画图就可以解决问题。不想画图,也可以较容易地观察:A中60>70,所以要建立小根堆,第二层70和65均大于60,但第三层的50显然小于70,不满足堆,排除;B中100>60,所以要建立大根堆,第二层60和70均小于100,第三层的50和32小于其父亲60,65小于其父亲70,满足大根堆,故选择B;C、D同理,可分析得不满足堆得性质。


建堆

给我们一个数组,怎样将其建为堆呢?向下调整算法 是最常用的方法,其更高效、更稳定。

向下调整算法的基本思想是 从根节点开始,逐步将父节点与子节点进行比较和必要的交换,直到整个堆满足堆的性质。

向下调整算法的前提是 左右子树均为堆。

向下调整算法的结果是根结点最终到达能使整棵树满足堆性质的位置

向下调整算法的基本思想中涉及父结点与子结点的交换,其规则为:

  • 如果要建立小根堆,则将其与左右孩子中较小的一个作比较,如果比 较小孩子 大(证明较小孩子是父、左右孩子三个结点中最小的),将根结点与较小孩子交换,到达新位置,继续与新的左右孩子的较小值比较,如果比 较小孩子 大,交换,以此类推,直到某次比 较小孩子 小或不存在孩子,结束。
  • 如果要建立大根堆,则将其与左右孩子中较大的一个作比较,如果比 较大孩子 小(证明较大孩子是父、左右孩子三个结点中最大的),将根结点与较大孩子交换,到达新位置,继续与新的左右孩子的较大值比较,如果比 较大孩子 小,交换,以此类推,直到某次比 较大孩子 大或不存在孩子,结束。

下图演示了对根结点27进行向下调整算法建小根堆的过程:

在这里插入图片描述


但是,我们不能保证所给数组天生满足根结点的左右子树均为堆,即不能从整棵的根结点开始执行向下调整算法。

既然不能从根结点开始向下调整,我们考虑从叶子结点开始执行向下调整,但叶子结点不存在左右孩子,即叶子结点作为根结点的树本身就满足堆,所以我们要从倒数第一个非叶子结点开始

堆存储在数组里,我们怎么确定倒数第一个非叶子结点的下标呢?

倒数第一个非叶子结点就是倒数第一个结点(即数组最后一个元素)的父结点,根据公式parent = (child - 1) / 2child传入数组最后一个元素的下标,就能得到向下调整算法开始执行的结点。

对倒数第一个非叶子结点执行向下调整算法后,继续对倒数第二个结点执行,以此类推,直到整棵树的根结点执行完向下调整算法,即可建堆成功!


代码实现:(以建小堆为例)

    public void createHeap(int[] array) {//从倒数第一个非叶子结点开始向下调整for(int i = (array.length - 1 - 1) / 2; i >= 0; i--) {//向下调整代码:int parent = i;int child = parent * 2 + 1;while(child < array.length) {//寻找较小结点if (child + 1 < array.length && array[child + 1] < array[child]) {child += 1;}//无需再判断child + 1 < array.lengthif (array[parent] > array[child]) {int tmp = array[parent];array[parent] = array[child];array[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}}
  • 每一次向下调整: 进循环前先保存父结点下标,再计算出左孩子的下标(假设左孩子是较小孩子);判断child下标是否存在,尝试进入循环;进入循环后,寻找较小孩子,(注意判断条件必须包含child + 1 < array.length且必须是第一个条件)如果之前认为左孩子是较小孩子的假设不成立,那么就更新child的值;判断父结点和较小孩子结点的大小,如果父结点小于较小结点,证明父结点是三个结点中最小的,直接跳出循环,否则,交换较小孩子和父结点,更新parent,接着计算新的父结点的左孩子下标并更新。继续尝试进入循环…

  • 特别注意当找到较小结点后,判断父结点和较小结点大小时,不需要先判断child + 1 < array.length,因为

    参与调整的结点一定存在孩子,否则不会进入while循环,一定存在则child的值是有效值。可能存在调整结点只有左孩子没有右孩子的情况,这时在寻找较小结点时,child的值不会变化,表示存在的左孩子的下标,调整的结点只要有孩子,就必须与孩子进行比较来维持堆的结构,如果此时判断父结点和较小结点大小时,加上child + 1 < array.length条件,就会因为调整结点没有右孩子而导致左孩子没有与父结点比较,这导致建堆可能失败。例如非堆序列 [32,50,100,70,65,60] ,如果犯了上述错误,则建堆结果与原序列一致,建堆失败!


堆的插入

当向堆中插入新元素时,先将其存储在数组已有元素后一个位置,然后 向上调整 到合适的位置以保证堆的结构。

下图为向一个大根堆中插入新元素11的向上调整的过程,整个过程要不断与父亲结点比较并不断上调,直到满足堆的性质。

实现堆(push/pop)_堆的pop-CSDN博客

    public void offer(int val) {if(isFull()) {throw new ElemIsFullException("the elemArray is full!:已满");}elem[usedSize] = val;shiftUp(usedSize);usedSize++;}//向上调整private void shiftUp(int child) {int parent = (child - 1) / 2;while(parent >= 0) {if(elem[child] < elem[parent]) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;child = parent;parent = (child - 1) / 2;}else {break;}}}
  • 此处堆插入的代码实现是模拟实现PriorityQueue代码的一部分,掌握插入的逻辑以及向上调整算法。其余的代码会在模拟实现阶段再见。

堆的删除

堆的删除一般指删除堆顶元素,具体操作是:将堆顶元素与最后一个元素交换,然后在认为最后一个元素被删除的情况下向下调整,继续维持堆的结构。

    public int poll() {if(isEmpty()) {throw new ElemIsEmptyException("elem is empty!:无元素可以删除!");}int ret = elem[usedSize-1];elem[usedSize-1] = elem[0];elem[0] = ret;//注意交换后认为元素已被删除,接下来要向下调整维持堆结构usedSize--;shiftDown(0, usedSize);return ret;}//向下调整private void shiftDown(int root, int len) {int parent = root;int child = parent * 2 + 1;while(child < len) {if (child + 1 < len && elem[child + 1] < elem[child]) {child += 1;}if (elem[parent] > elem[child]) {int tmp = elem[parent];elem[parent] = elem[child];elem[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}
  • 此处堆删除的代码实现是模拟实现PriorityQueue代码的一部分,掌握删除的逻辑是先交换头尾再向下调整维持堆结构。其余的代码会在模拟实现阶段再见。

堆的应用

堆最常见的应用除了实现优先级队列外,还有:

  • 堆排序
  • TopK问题
  • 频率统计和数据压缩
  • 合并有序小文件

堆排序是十分重要的一种排序方法,后续会在排序章节介绍,TopK问题是本文最后一个问题。


PriorityQueue

在Java的集合框架中,PriorityQueue 是一个基于优先级堆实现的的无界优先级队列。

其基本特性如下:

  • 不允许使用 null 元素也不允许插入不可比较的对象(即没有实现 Comparable 接口的对象,或者在创建时没有提供 Comparator 的情况下)。
  • 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 默认情况下为小根堆,即每次获取到的元素都是最小元素

更多具体内容如下:

PriorityQueue的构造方法

构造方法说明
PriorityQueue()创建一个空的优先级队列,默认容量为11
PriorityQueue(int initialCapacity)创建一个初始容量为initalCapacity的优先级队列,但不能指定小于1,否则会抛出异常
PriorityQueue(Collection<? extends E> c)创建一个包含指定集合元素的 PriorityQueue优先级队列
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)创建一个具有指定初始容量和指定比较器的空 PriorityQueue优先级队列
PriorityQueue(Collection<? extends E> c, Comparator<? super E> comparator)创建一个包含指定集合元素的 PriorityQueue,并根据提供的 Comparator 进行排序。

PriorityQueue默认是小顶堆,如果我们需要一个大顶堆,则需要提供一个自定义的Comparator来实现这一点,该自定义的Comparator需要反转排序规则,这样就能得到一个大顶堆。

Comparator.reverseOrder() 是 Java 中 Comparator 接口的一个静态方法,它返回了一个实现了 Comparator 接口的 Comparator 实例,这个实例可以对实现了 Comparable 接口的对象集合进行逆序排序。换句话说,它创建了一个比较器,该比较器会将较大的元素视为较低(或“更好”的)优先级,从而与默认的自然顺序(通常是升序)相反。

例如:

//默认小根堆
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();//指定为大根堆
PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(Comparator.reverseOrder());

PriorityQueue的常用方法

方法功能
boolean offer(E e)插入元素,注意插入的元素不能为空且要可比较
E peek()获取优先级最高的元素(堆顶元素),如果优先级队列为空,返回null
E poll()删除优先级最高的元素(堆顶元素),如果优先级队列为空,返回null
int size()获取有效元素个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空

PriorityQueue的模拟实现

以int类型为例对关键逻辑进行了实现,同时模拟实现的没有涉及扩容,这与PriorityQueue不同,我们主要掌握建堆、堆的删除、堆的插入操作。

public class ExceedTheCapacityException extends RuntimeException {public ExceedTheCapacityException(String message) {super(message);}
}public class ElemIsFullException extends RuntimeException {public ElemIsFullException(String message) {super(message);}
}public class ElemIsEmptyException extends RuntimeException {public ElemIsEmptyException(String message) {super(message);}
}public class MyPriorityQueue {public int[] elem;public int usedSize;//默认分配的容量private static final int DEFAULT_INIT_CAPACITY = 11;public MyPriorityQueue() {this(DEFAULT_INIT_CAPACITY);}public MyPriorityQueue(int capacity) {this.elem = new int[capacity];}private void initElem(int[] array) {for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}public void createHeap(int[] array) {if(array.length > elem.length) {throw new ExceedTheCapacityException("exceed the capacity!:传入的数组超出了容量");}initElem(array);for(int i = (array.length - 1 - 1) / 2; i >= 0; i--) {shiftDown(i, this.usedSize);}}private void shiftDown(int root, int len) {int parent = root;int child = parent * 2 + 1;while(child < len) {//寻找较小结点if (child + 1 < len && elem[child + 1] < elem[child]) {child += 1;}//无需再判断child + 1 < array.lengthif (elem[parent] > elem[child]) {int tmp = elem[parent];elem[parent] = elem[child];elem[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}public void offer(int val) {if(isFull()) {throw new ElemIsFullException("the elemArray is full!:已满");}elem[usedSize] = val;shiftUp(usedSize);usedSize++;}private void shiftUp(int child) {int parent = (child - 1) / 2;while(parent >= 0) {if(elem[child] < elem[parent]) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;child = parent;parent = (child - 1) / 2;}else {break;}}}public boolean isFull() {return elem.length == usedSize;}public int poll() {if(isEmpty()) {throw new ElemIsEmptyException("elem is empty!:无元素可以删除!");}int ret = elem[usedSize-1];elem[usedSize-1] = elem[0];elem[0] = ret;usedSize--;shiftDown(0, usedSize);return ret;}public boolean isEmpty() {return usedSize == 0;}public int peek() {return elem[0];}
}

经典TopK问题

TopK问题,即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如,世界500强,游戏排行榜。

TopK问题一定可以使用排序来解决问题,但是当数据量比较庞大时,排序就很难高效解决问题了。这时,使用堆就能更好的解决问题,具体思想如下:

  1. 用数据集合中前K个元素来建堆
    • 前K个最大的元素,建小堆
    • 前K个最小的元素,建大堆
  2. 用剩余的N-K个元素依次与堆顶元素进行比较,不满足则替换堆顶元素

例如,对于500条数据,寻找前K个最大的数据元素,将集合前K个元素建立小堆,此时堆顶元素就是这K个建堆元素中最小的,也是最有可能被替换的,然后,遍历剩余的N-K个元素,分别与堆顶元素比较,如果比堆顶元素大,说明该元素可以入选当前的前10,替换堆顶元素。我们可以利用PriorityQueue很好的解决这一类问题,而不需要特意编写堆的插入和删除代码,这是我们学习集合框架的意义。

【例题】

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

class Solution {public int[] smallestK(int[] arr, int k) {//补充代码}
}

利用上述思路即可解决问题:

class Solution {public int[] smallestK(int[] arr, int k) {int[] ret = new int[k];if(k == 0) {return ret;}//“小堆转大堆”PriorityQueue<Integer> q = new PriorityQueue<>(Comparator.reverseOrder());//将前k个元素offerfor(int i = 0; i < k; i++) {q.offer(arr[i]);}//遍历剩余元素,与堆顶元素比较,如果比堆顶元素小,则删除现堆顶,插入该元素for(int i = k; i < arr.length; i++) {if(arr[i] < q.peek()) {q.poll();q.offer(arr[i]);}}//将PriorityQueue中的元素都放入数组中,返回for(int i = 0; i < k; i++) {ret[i] = q.poll();}return ret;}
}

原题链接:面试题 17.14. 最小K个数 - 力扣(LeetCode)


这篇关于Java数据结构(七)——优先级队列与PriorityQueue的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

springboot将lib和jar分离的操作方法

《springboot将lib和jar分离的操作方法》本文介绍了如何通过优化pom.xml配置来减小SpringBoot项目的jar包大小,主要通过使用spring-boot-maven-plugin... 遇到一个问题,就是每次maven package或者maven install后target中的ja

Java中八大包装类举例详解(通俗易懂)

《Java中八大包装类举例详解(通俗易懂)》:本文主要介绍Java中的包装类,包括它们的作用、特点、用途以及如何进行装箱和拆箱,包装类还提供了许多实用方法,如转换、获取基本类型值、比较和类型检测,... 目录一、包装类(Wrapper Class)1、简要介绍2、包装类特点3、包装类用途二、装箱和拆箱1、装

如何利用Java获取当天的开始和结束时间

《如何利用Java获取当天的开始和结束时间》:本文主要介绍如何使用Java8的LocalDate和LocalDateTime类获取指定日期的开始和结束时间,展示了如何通过这些类进行日期和时间的处... 目录前言1. Java日期时间API概述2. 获取当天的开始和结束时间代码解析运行结果3. 总结前言在J

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J