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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

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

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1180(广搜+优先队列)

此题要求最少到达目标点T的最短时间,所以我选择了广度优先搜索,并且要用到优先队列。 另外此题注意点较多,比如说可以在某个点停留,我wa了好多两次,就是因为忽略了这一点,然后参考了大神的思想,然后经过反复修改才AC的 这是我的代码 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听