【数据结构】(9) 优先级队列(堆)
一、优先级队列
优先级队列不同于队列,队列是先进先出,优先级队列是优先级最高的先出。一般有两种操作:返回最高优先级对象,添加一个新对象。
二、堆
2.1、什么是堆
堆也是一种数据结构,是一棵完全二叉树,该完全二叉树中的所有子树的根都大于其孩子,即大根堆;如果都小于其孩子,就是小根堆。
2.2、堆的存储方式
由于完全二叉树按层序遍历,节点之间没有空隙,所以存储在顺序表中不会造成空间浪费;并且顺序存储方便访问二叉树中某节点 i 的双亲((i-1)/2)和孩子(左:2*i+1,右:2*i+2),以便调整堆。因此,堆采用顺序结构存储。
大根堆示例:
三、优先级队列(堆)的模拟实现
PriorityQueue 底层是用堆实现的。
3.1、将完全二叉树调整为堆(向下调整)
3.1.1、手动模拟过程
调整下面的完全二叉树为堆。
从最后一个父节点开始调整(parent=((usedSize-1)-1)/2):将较大的孩子 child 与父节点比较(没有右孩子不需要比较孩子大小),若 child 更大则于父节点交换。
调整倒数第 2 棵子树(parent--)。
调整倒数第 3 棵子树。
调整倒数第 4 棵子树。
如果p和c交换了,还要调整其子树(p=c),直到 p 和 c 没有交换为止,或者 c 超过二叉树大小 usedSize 为止。(向下调整)
重复上述操作,直到调整完 p=0 的树。
3.1.2、代码实现
向下调整过程,时间复杂度推导:如果某棵子树高为 k,则最多交换 k-1 次,高为 k 的完全二叉树最多有 N=(2^k)-1 个结点,因此 k-1 = (log(N+1)-1),时间复杂度为 O(logN)。
/*** 向下调整* 时间复杂度:O(logN)* @param parent 子树根节点的下标* @param size 子树的大小*/private void shiftDown(int parent, int size) {int maxChild = 2*parent+1; // 默认最大孩子为左孩子while(maxChild < size) { // 存在左孩子就继续调整if(maxChild+1 < size && arr[maxChild+1] > arr[maxChild]) { // 存在右孩子且右孩子大于左孩子maxChild++;}if (arr[maxChild] > arr[parent]) { // 最大孩子大于父节点,交换swap(parent, maxChild);parent = maxChild; // 继续调整子树maxChild = 2*parent+1;}else { // 没有交换,结束调整break;}}}
将完全二叉树调整为堆,时间复杂度推导:
T=1*(h-1)+2^1*(h-2)+……+2^(h-2)*1。
2T=2^1*(h-1)+2^2*(h-2)+……+2^(h-1)*1。
2T-T=T=2^1+2^2+……+2^(h-2)+2^(h-1)-h+1=2^h-1-h=2^log(N+1)-1-log(N+1)=N-log(N+1)
时间复杂度为 O(N)。
/*** 将一棵完全二叉树调整为大根堆* 时间复杂度:O(N)*/public void shift2Heap() {int initParent = ((usedSize - 1)-1)/2;for (int i = initParent; i >= 0; i--) {shiftDown(i, usedSize);}}
测试结果:
3.2、堆中插入一个新节点(向上调整)
另一种创建堆的方法是,每插入一个新节点,就调整二叉树为堆。
3.2.1、手动模拟过程
在结尾插入新结点80,并从最后一棵子树开始(child=usedSize-1),从下往上调整,直到 parent=0,或者没有交换为止。(向上调整)
child=parent。
3.2.2、代码实现
向上调整:
/*** 向上调整* 时间复杂度:O(logN)* @param child 添加的孩子节点的下标*/private void shiftUp(int child) {int parent = (child-1)/2; // 父节点下标while(parent >= 0 && arr[child] > arr[parent]) { // 父节点存在且孩子大于父节点,交换swap(child, parent);child = parent;parent = (child-1)/2; // 继续向上调整}}
插入一个新节点:
/*** 添加元素到堆中* 时间复杂度:O(NlogN)*/public void offer(int val) {if (isFull()) { // 数组已满,扩容arr = Arrays.copyOf(arr, arr.length * 2);}arr[usedSize] = val;usedSize++;shiftUp(usedSize-1); // 向上调整}
测试:
如果插入 N 个结点,时间复杂度推导:
T=2*1+2^2*2+……+2^(h-2)*(h-2)+2^(h-1)*(h-1)
2T=2^2+2^3*2+……+2^(h-1)*(h-2)+2^h*(h-1)
2T-T=T=2^h*(h-1)-2^2-2^3-……-2^(h-1)-2=2^h*(h-1)-2*(2^(h-1)-1)
=2^h*(h-1)-2^h+2=2+2^h*(h-2)+2=(N+1)*(log(N+1)-2)+2
时间复杂度为:O(NlogN)
3.3、删除堆顶元素
步骤:
- 堆顶与堆尾元素交换。
- 删除堆尾元素。
- 对堆从树根开始向下调整。
public int poll() {if (isEmpty()) {return -1;}int ret = arr[0];arr[0] = arr[usedSize-1];usedSize--;shiftDown(0, usedSize); // 向下调整return ret;}
四、PriorityQueue 的使用
4.1、注意事项
- PriorityQueue 是线程不安全的,PriorityBlockingQueue 是线程安全的。
- PriorityQueue 中放置的元素必须是可比较大小的,否则会抛出异常。
- 不能插入 null,否则会抛出异常。
而我们之前学习的 LinkList 是可以插入 null 的:
- 默认构建小根堆。
4.2、构造函数的使用
无参构造器:默认容量 11,默认比较器为空。
带初始容量参数的构造器:指定初始容量,默认比较器为空。
用一个集合创建优先级队列:传入某集合对象。
因为 String 不是 Number 及其子类,所以语法错误。
Integer 是 Number 的子类,成功调用。
指定初始容量、比较器的构造函数:
transient 的作用:让不想被序列化的属性不被序列化,保证信息的隐私,比如密码。序列化就是把对象的信息转成字符串放到磁盘里;反序列化就是把磁盘里的字符串转成对象信息。transient 就让修饰的属性信息的生命周期仅在调用者的内存中,而不会写进磁盘中持久化。
4.3、offer 插入一个元素
插入一个元素,offer 源码:
扩容,grow 源码:
向上调整,shiftUp 源码:
如果元素是基础类型的包装类,会使用自身重写的 compareTo:
如果构造函数参数指定了比较器:
4.4、poll 删除堆顶元素
删除堆顶元素,poll 源码:
可以看到源码的向下调整的方法,与我们实现的方法大致相同。
五、OJ练习
5.1、top-k 问题:最小的 k 个数
面试题 17.14. 最小K个数 - 力扣(LeetCode)
- 解法1:用排序算法,从小到大排序,取前 k 个。最快的排序算法时间复杂度 O(NlogN)。
- 解法2:创建小根堆,取 k 次堆顶元素,每次删除堆顶元素后,向下调整。时间复杂度 O(NlogN)。
/*** 方法1:使用小根堆实现topK小,* 时间复杂度:O(NlogN) + O(k*logN) = O(NlogN)*/public static int[] topK1(int[] arr, int k) {if(k > arr.length) { // k大于数组长度,直接返回数组return arr;}if(k <= 0) { // k小于等于0,返回空数组return new int[0];}// 创建一个小根堆,O(NlogN)PriorityQueue<Integer> pq = new PriorityQueue<>(k);for (int x : arr) {pq.offer(x);}// 取k次堆顶元素,存入数组,O(k*logN)int[] ret = new int[k];for (int i = 0; i < k; i++) {ret[i] = pq.poll();}return ret;}
- 解法三:创建大小为 k 的大根堆,从 k 开始遍历数组,遇到比堆顶(堆中的最大值)小的 x,就删除堆顶,插入 x。效率最高,O(N*logk)。
/*** 方法2:使用大根堆实现topK小,* 时间复杂度:O(k*logk) + O((N-k)*logk) + O(k*logk) = O(N*logk)*/public static int[] topK2(int[] arr, int k) {if(k > arr.length) { // k大于数组长度,直接返回数组return arr;}if(k <= 0) { // k小于等于0,返回空数组return new int[0];}// 自定义比较器,实现大根堆Comparator<Integer> cmp = new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);}};// 创建一个大小为 k 的大根堆,O(k*logk)PriorityQueue<Integer> pq = new PriorityQueue<>(k, cmp);for (int i = 0; i < k; i++) {pq.offer(arr[i]);}// 遍历数组,如果元素小于堆顶元素,替换堆顶元素,并调整堆,O((N-k)logk)for (int i = k; i < arr.length; i++) {if (arr[i] < pq.peek()) {pq.poll();pq.offer(arr[i]);}}// 取出堆中的元素,存入数组,O(k*logk)int[] ret = new int[k];for (int i = 0; i < k; i++) {ret[i] = pq.poll();}return ret;}
5.2、堆排序
将序列从小到大排序:
- 方法1:创建小根堆,每次取堆顶,放入一个新的数组中。
空间复杂度:O(N),创建了一个新数组存放结果。当数据很多时,浪费内存。
时间复杂度:O(NlogN)。
- 方法2:创建大根堆,执行 N-1 次循环,每次将堆顶最大元素与未排序的堆尾元素交换,交换后对未排序的部分进行调整。
空间复杂度:O(1)
时间复杂度:O(NlogN)
初始状态:
第一次交换,并调整:
第二次交换,并调整:
// 堆排序public void sort() {for (int i = usedSize-1; i > 0; i--) {// 交换堆顶和最后一个未排序的元素swap(0, i);// 向下调整堆,元素 i-1 已排序shiftDown(0, i-1);}}
相关文章:

【数据结构】(9) 优先级队列(堆)
一、优先级队列 优先级队列不同于队列,队列是先进先出,优先级队列是优先级最高的先出。一般有两种操作:返回最高优先级对象,添加一个新对象。 二、堆 2.1、什么是堆 堆也是一种数据结构,是一棵完全二叉树,…...
如何提升爬虫获取数据的准确性?
提升爬虫获取数据的准确性是确保数据分析和后续应用有效性的关键。以下是一些经过验证的方法和最佳实践,可以帮助提高爬虫数据的准确性: 1. 数据清洗 数据清洗是提升数据准确性的重要步骤,主要包括去除重复数据、处理缺失值和异常值。 去除…...
Obsidian及Zotero常用的插件
Obsidian插件 Minimal Theme Settings(Life,zotero)【必需】 界面样式设置所需插件 Style Settings(Life,zotero)【必需】界面样式设置所需插件 Recent Files(Life,zotero…...

闲鱼IP属地是通过电话号码吗?
在闲鱼这样的二手交易平台上,用户的IP属地信息对于维护交易安全、增强用户间的信任至关重要。然而,关于闲鱼IP属地是如何确定的,不少用户存在疑惑,尤其是它与电话号码之间是否存在关联。本文将深入探讨这一问题,揭示闲…...
C#多线程异步连接MySQL与SQLserver数据库
C#多线程异步连接MySQL与SQLserver数据库 一、前言二、多线程异步连接数据库代码2.1代码块2.2代码说明 参考文档 一、前言 当编写代码连接多台设备上的数据库时,如果采用同步逐个连接的方式,在网络畅通的情况下连接速度尚可,但当其中一台设备…...

51单片机-数码管
目录 1、静态数码管 1.1、数码管是如何显示出字符 1.2、数码管静态显示原理 1.3、74HC573芯片的使用 1.4、静态数码管编程 2、动态数码管 2.1、数码管动态显示原理 2.2、74HC138芯片的使用 2.3、编写动态数码管程序 1、静态数码管 1.1、数码管是如何显示出字符 单片机…...
C#学习之S参数读取(s2p文件)
目录 一、创作灵感 二、S2PFileReader类 1.代码示例 2.代码说明 a.ReadS2PFile 方法: b.DataTable 结构: 三、S2PFileReader类的调用演示 1.使用示例 一、创作灵感 虽然MATLAB处理数据很实用,但是C#常用于程控仪器的控制,…...
Spring Boot “约定大于配置”
什么是“约定大于配置”? “约定大于配置”是一种简化开发的设计理念。简单来说,就是框架默认提供了常见的配置和行为,开发者只需要按照约定来编写代码,避免了繁琐的配置,只在需要时进行定制和调整。这种理念在Spring…...

传输层协议TCP ( 下 )
文章目录 前言序号与确认序号超时重传RTOJacobson算法内核中超时时间的计算 滑动窗口滑动窗口延迟应答流量控制 拥塞控制慢启动拥塞避免快重传快速恢复 保活机制参考资料 前言 TCP(Transmission Control Protocol,传输控制协议)是互联网最重要…...

NLP 八股 DAY1:BERT
BERT全称:Pre-training of deep bidirectional transformers for language understanding,即深度双向Transformer。 模型训练时的两个任务是预测句⼦中被掩盖的词以及判断输⼊的两个句⼦是不是上下句。在预训练 好的BERT模型后⾯根据特定任务加上相应的⽹…...

演示synchronized锁机制用法的简单Demo
演示synchronized锁机制用法的简单Demo。我们以"银行开户"场景为例:每个用户只能创建一个账户(模拟类似原代码中每个用户只能有一个私有空间的限制)。 第1步:创建项目结构 demo-lock ├── src/main/java/com/exampl…...

Datawhale 数学建模导论二 笔记1
第6章 数据处理与拟合模型 本章主要涉及到的知识点有: 数据与大数据Python数据预处理常见的统计分析模型随机过程与随机模拟数据可视化 本章内容涉及到基础的概率论与数理统计理论,如果对这部分内容不熟悉,可以参考相关概率论与数理统计的…...
差分解方程
差分解方程 差分法在数值求解偏微分方程(PDEs)和常微分方程(ODEs)时,可以分为隐式格式和显式格式。以下是两者的主要区别: 显式格式(Explicit Scheme) 时间推进: 显式格…...

EasyExcel 复杂填充
EasyExcel Excel表格中用{}或者{.} 来表示包裹要填充的变量,如果单元格文本中本来就有{、}左右大括号,需要在括号前面使用斜杠转义\{ 、\}。 代码中被填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。 …...
ESP32通过MQTT连接阿里云平台实现消息发布与订阅
文章目录 前言 一、准备工作 二、阿里云平台配置 三、代码实现 总结 前言 本文将介绍如何使用ESP32开发板通过MQTT协议连接阿里云物联网平台,并实现消息的发布与订阅功能。我们将使用Arduino IDE进行开发,并借助PubSubClient库实现MQTT通信。 一、准备…...

NVIDIA Jetson Orin Nano 刷机过程
1. 背景 新到手 NVIDIA Jetson Orin Nano 插上显示屏,显示如下: 这是UEFI Shell,UEFI Shell(统一可扩展固件接口外壳程序)是一种基于UEFI规范的交互式命令行工具,它运行在UEFI固件环境中,为用…...
C#学习之数据转换
目录 一、创作说明 二、数据类型之间的转换 1.数据类型之间的转换表格 2.代码示例 三、进制之间的转换 1.进制之间的转换表格 2.代码示例 四、ASCII 编码和字符之间的转换 1.ASCII 编码和字符之间的转换表格 2.代码示例 五、总结 一、创作说明 C#大多数时候都是和各…...

typecho快速发布文章
typecho_Pytools typecho_Pytools工具由python编写,可以快速批量的在本地发布文章,不需要登陆后台粘贴md文件内容,同时此工具还能查看最新的评论消息。… 开源地址: GitHub Gitee 使用教学:B站 一、主要功能 所有操作不用登陆博…...

深度学习R4周:LSTM-火灾温度预测
🍨 本文为🔗365天深度学习训练营中的学习记录博客🍖 原作者:K同学啊 任务: 数据集中提供了火灾温度(Tem1)、一氧化碳浓度(CO 1)烟雾浓度(Soot 1)…...
探索Java中的集合类_特性与使用场景
1. 引言 1.1 Java集合框架概述 Java集合框架(Java Collections Framework, JCF)是Java中用于存储和操作一组对象的类和接口的统称。它提供了多种数据结构来满足不同的需求,如列表、集合、映射等。JCF的核心接口包括Collection、List、Set、Queue和Map,以及它们的各种实现…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...

华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...

回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...
【WebSocket】SpringBoot项目中使用WebSocket
1. 导入坐标 如果springboot父工程没有加入websocket的起步依赖,添加它的坐标的时候需要带上版本号。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dep…...