数据结构——优先级队列(堆)Priority Queue详解

1. 优先级队列

队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该场景下,使用队列不合适

在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象,这种数据结构就是优先级队列(Priority Queue)

2. 优先级队列的模拟实现

2.1 堆的概念

性质:

如上图,堆中某个节点的值总是不大于其父节点的值,称为最小堆/小根堆

堆中某个节点的值总是不小于其父节点的值,称为最大堆/大根堆

堆总是一棵完全二叉树

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可用层序的规则采用顺序的方式进行高效存储

tip:对于非完全二叉树则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率较低

将元素存储到数组中后,假设 i 为节点在数组中的下标,则有:

  • 如果 i 为0,则 i 表示的节点为根节点,否则 i 节点的双亲结点为(i - 1)/2
  • 如果 2*i+1 小于节点个数,则节点 i 的左孩子下标为 2*i+1 ,否则没有左孩子
  • 如果 2*i+2 小于节点个数,则节点 i 的右孩子下标为 2*i+2 ,否则没有右孩子

2.3 堆的创建

2.3.1 堆向下调整

以集合{ 27,15,19,18,28,34,65,49,25,37 }为例,转换为大根堆的过程:

//TestHeap 类
public class TestHeap {public int[] elem;//堆由数组实现public int usedSize;//记录有效数据个数public TestHeap() {//构造方法this.elem = new int[10];}public void init(int[] array) {//给数组初始化for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}//把elem数组中的数据调整为大根堆public void createHeap() {//让parent初始位置在树的最后一棵子树的根节点处,用到公式:根节点 = (i-1)/2//其中usedSize-1表示最后一个节点,从上到下循环遍历整棵树for (int parent = (usedSize-1-1)/2; parent >= 0; parent--) {//siftDown方法用来判断树中左右孩子与父亲节点的大小关系siftDown(parent,usedSize);}}//交换函数public void swap(int i, int j) {int tmp = elem[i];elem[i] = elem[j];elem[j] = tmp;}public void siftDown(int parent, int end) {int child = 2*parent+1;//公式:左孩子下标 = 2*根节点下标+1while(child < end) {//end是有效数据个数,不能用<=//下面if走完之后,child一定是左右孩子中最大值的下标if(child+1 < end && elem[child] < elem[child+1]) {child++;}//下面if判断孩子节点是否大于父亲节点,若大于则交换,否则跳出循环,去判断上一棵树if(elem[child] > parent) {swap(child,parent);parent = child;child = 2*parent+1;}else {break;}}}
}//Test 类
public class Test {public static void main(String[] args) {int[] array = {27,15,19,18,28,34,65,49,25,37};TestHeap testHeap = new TestHeap();testHeap.init(array);testHeap.createHeap();}
}

tip:在调整以parent为根的二叉树时,必须要满足parent的左子树和右子树已经是堆,才可以向下调整

时间复杂度:最坏的情况就如上例,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(\log _{2}N)

2.3.2 建堆的时间复杂度

推导:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了方便计算,使用满二叉树来推导

结论: 建堆的时间复杂度为 O(N)

2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入需要两步

1. 先将元素放到树底层最后一个节点(空间不够时扩容)

2. 将插入新节点向上调整,直到满足堆的性质

    public void offer(int val) {if(isFull()) {//判满,若满则扩容elem = Arrays.copyOf(elem,2*elem.length);}elem[usedSize] = val;//val赋值到usedSize位置,usedSize++usedSize++;siftUp(usedSize-1);//向上调整}public void siftUp(int child) {int parent = (child-1)/2;//通过孩子节点找到父亲节点while(parent >= 0) {//当父亲节点下标小于0,说明向上调整结束了,堆已有序if(elem[child] > elem[parent]) {//孩子节点的值大于父亲节点值,即交换swap(child,parent);child = parent;parent = (child-1)/2;}else {//若孩子节点的值小于父亲节点的值,说明堆已有序,直接跳出循环break;}}}public boolean isFull() {return usedSize == elem.length;}

2.4.2 堆的删除

tip:堆的删除一定删除的是堆顶元素,分为三步:

1. 将堆定元素对堆中最后一个元素交换

2. 将堆中有效数据个数减少一个

3. 对堆顶元素进行向下调整

    public int poll() {if(isEmpty()) {//若栈空,返回-1return -1;}int old = elem[0];//记录要删除的数据,最后返回swap(0,usedSize-1);//交换堆顶元素和最后元素usedSize--;//有效个数--siftDown(0,usedSize);//从堆定开始,向下调整return old;}public boolean isEmpty() {return usedSize == 0;}

例题:

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

A: 100,60,70,50,32,65   B: 60,70,65,50,32,100   C: 65,100,70,32,50,60

D: 70,65,100,32,50,60   E: 32,50,100,70,65,60   F: 50,100,70,65,60,32

选A

2.已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()

A: 1         B: 2          C: 3        D: 4

解析:选C,15与10比,12与10比,12与16比

3.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A: [3,2,5,7,4,6,8]        B: [2,3,5,7,4,6,8]

C: [2,3,4,5,7,8,6]        D: [2,3,4,5,6,7,8]

选C

3. 常用接口介绍

3.1 PriorityQueue的特性

Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue

    public static void main(String[] args) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();priorityQueue.offer(1);priorityQueue.offer(2);priorityQueue.offer(3);System.out.println(priorityQueue.poll());//运行结果:1System.out.println(priorityQueue.poll());//        2}

关于PriorityQueue的使用注意:

  • 1.使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
  • 2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常 
  • 3. 不能插入null对象,否则会抛出 NullPointerException
  • 4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 5. 插入和删除元素的时间复杂度为O(log_2N)
  • 6. PriorityQueue底层使用了堆数据结构
  • 7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

3.2 PriorityQueue常用接口介绍

3.2.1 优先级队列的构造

此处只是常见的几种:

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常
PriorityQueue(Collection c)用一个集合来创建优先级队列

构造方法原理:

用一个集合来创建优先级队列:

class Student {}
public class Test {public static void main(String[] args) {ArrayList<Integer> arrayList = new ArrayList<>();arrayList.add(1);arrayList.add(2);arrayList.add(3);PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(arrayList);priorityQueue.offer(4);priorityQueue.offer(5);priorityQueue.offer(6);System.out.println(priorityQueue.poll());//运行结果:1System.out.println(priorityQueue.poll());//        2PriorityQueue<Student> priorityQueue1 = new PriorityQueue<>(arrayList);//error//传入参数必须是Student类或其子类}
}

3.2.2 offer元素原理:

查看源码:

结合上面创建小根堆的流程分析:

自己实现比较器:

//实现小根堆的比较器
class IntCmp implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o1.compareTo(o2);//小根堆}
}
public class Test {public static void main(String[] args) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());//别忘了此处new比较器对象作为参数priorityQueue.offer(4);priorityQueue.offer(5);priorityQueue.offer(6);System.out.println(priorityQueue.poll());//运行结果:4System.out.println(priorityQueue.poll());//        5}
}//实现大根堆的比较器
class IntCmp implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);//大根堆 两比较器仅有此处不同!!!!!!!!!!!!!!}
}
public class Test {public static void main(String[] args) {PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());priorityQueue.offer(4);priorityQueue.offer(5);priorityQueue.offer(6);System.out.println(priorityQueue.poll());//运行结果:6System.out.println(priorityQueue.poll());//        5}

3.2.3 PriorityQueue的扩容方式

以下是JDK17中的源码:

说明:

如果容量小于64时,按照oldCapacity的2倍方式扩容

如果容量大于等于64,按照oldCapacity的1.5倍方式扩容

如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

3.3 OJ练习

top-k问题:最大或最小的前k个数据

    public static int[] smallestK(int[] arr, int k) {PriorityQueue<Integer> minHeap = new PriorityQueue<>();for (int i = 0; i < arr.length; i++) {minHeap.offer(arr[i]);}int[] tmp = new int[k];for (int i = 0; i < k; i++) {int val = minHeap.poll();tmp[i] = val;}return tmp;}public static void main(String[] args) {int[] array = {27,15,19,18,28};int[] ret = smallestK(array,3);System.out.println(Arrays.toString(ret));}

运行结果:

虽然上述方法也能满足需求,但是其时间复杂度为O((N+K)*\log _{2}N)

不是非常好的解决方案,现需要一个时间复杂度更小的方法:

若求前K个最小的数字

1. 先把前 个元素建立大小为K的大根堆

2. 遍历剩余 N-K 个元素,若堆顶元素大于当前 i 下标的值就出堆

这样遍历完数组后,大小为 K 的堆中就是最小的 K 个元素

反之求前K个最大的数字就建立小根堆

class IntCmp implements Comparator<Integer> {@Overridepublic int compare(Integer o1, Integer o2) {return o2.compareTo(o1);//大根堆}
}
class Solution {public int[] smallestK(int[] arr, int k) {int[] tmp = new int[k];if(k == 0) {return tmp;}PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new IntCmp());//1.把前K个元素放进堆中for (int i = 0; i < k; i++) {maxHeap.offer(arr[i]);}//2.遍历剩下的 N-k 个元素for (int i = k; i < arr.length; i++) {int val = maxHeap.peek();if(val > arr[i]) {maxHeap.poll();maxHeap.offer(arr[i]);}}//3.将堆中元素放到数组中for (int i = 0; i < k; i++) {tmp[i] = maxHeap.poll();}return tmp;}
}

变种题:求第K小的数据,建立大根堆,堆顶元素就是第K小

3.4 堆排序

要求:在原堆上修改,不能新建

方法两步:

1. 建堆:

  • 要求升序,建大堆
  • 要求降序,建小堆

2. 利用堆删除来进行排序

建堆和堆删除中都用到了向下调整

    //堆排序 升序public void heapSort() {int endIndex = usedSize-1;while(endIndex > 0) {swap(0,endIndex);siftDown(0,endIndex);endIndex--;}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/31953.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代理ip服务器有哪些作用?

IP代理服务器具有以下几个作用&#xff1a; 1. 隐藏真实IP地址 代理服务器作为中间人&#xff0c;将用户请求转发给目标网站&#xff0c;从而隐藏用户的真实IP地址。这有助于保护用户的隐私和匿名性&#xff0c;防止被网站或其他人追踪和监控。 2. 绕过访问限制 某些网站可能…

2024.06.21 刷题日记

101. 对称二叉树 判断是否对称&#xff0c;检查 root->left->val root->right->val&#xff0c;接着进行递归检查对称位置&#xff1a; class Solution { public:// 传入对称位置的两个对称位置bool isMirror(TreeNode* left, TreeNode* right) {if (!left &…

设计模式2-面向对象设计原则

设计模式-面向对象的设计原则 依赖倒置原则开闭封闭原则单一职责原则Liskov替换原则接口隔离原则面向对象优先使用对象组合&#xff0c;而不是类继承。封装变化点针对接口编程&#xff0c;而不是针对实现编程 变化是复用的天地。面向对象设计最大的优势在于抵御变化。 重新认识…

odoo 翻译字段sql查询语句

字段写法&#xff1a; name->>en_US 任务&#xff1a; 查询name字段中&#xff0c;包含ring的数据 SQL模糊查询 SELECT * FROM product_public_category WHERE name->>en_US LIKE %ring%; SQL精准查询 SELECT * FROM product_public_category WHERE name->…

深入解析MVC架构(Model-View-Controller Architecture)

目录 前言1. MVC架构概述1.1 模型&#xff08;Model&#xff09;1.1.1 数据管理1.1.2 业务逻辑 1.2 视图&#xff08;View&#xff09;1.2.1 数据展示1.2.2 用户界面设计 1.3 控制器&#xff08;Controller&#xff09;1.3.1 用户输入处理1.3.2 更新模型和视图 2. MVC架构的优缺…

主干网络篇 | YOLOv5/v7 更换主干网络之 ResNet50/ResNet101 | 对比实验必备

主干网络篇 | YOLOv5/v7 更换主干网络之 ResNet50/ResNet101 | 对比实验必备 1. 简介 ResNet 是近年来最受欢迎的深度卷积神经网络架构之一&#xff0c;它以其优异的性能和鲁棒性而著称。ResNet50 和 ResNet101 是 ResNet 家族中最常用的两个模型&#xff0c;它们分别具有 50…

【深度学习】stable-diffusion-webui AUTOMATIC1111 的参数解释翻译

https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Command-Line-Arguments-and-Settings 参数命令值默认值描述-h, --helpNoneFalse显示帮助信息并退出–exit安装后终止–data-dirDATA_DIR./存储所有用户数据的基本路径–configCONFIGconfigs/stable-diffusion/…

易管理工厂设备日志采集工具

免费试用下载: Gitee下载 最新版本 优势: A. 开箱即用. 解压直接运行.不需额外安装. B. 批管理设备. 设备配置均在后台管理. C. 无人值守 客户端自启动,自更新. D. 稳定安全. 架构简单,内存占用小,通过授权访问.

通过python脚本采集网络流量

#!/usr/bin/python # -*- coding:utf-8 -*- psutil模块是一个跨平台的获取进程和系统应用情况&#xff08;CPU&#xff0c;内存&#xff0c;磁盘&#xff0c;网络&#xff0c;传感器&#xff09;的库。 该模块用于系统监控、限制进程资源和运行进程的管理等方面。 网络信息 ps…

反激开关电源输出假负载

1、为何需要假负载&#xff1f; 开关电源芯片的占空比最小不可能做到0%&#xff0c;都有一个最小导通时间&#xff0c;不过最小导通时间&#xff0c;在规格书中&#xff0c;不一定给出来 注意&#xff1a;如果没有最小导通时间&#xff0c;就相当于芯片都停止输出了&#xff…

29-Linux--守护进程

一.基础概念 1.守护进程&#xff1a;精灵进程&#xff0c;在后台为用户提高服务&#xff0c;是一个生存周期长&#xff0c;通常独立于控制终端并且周期性的执行任务火处理事件发生 2.ps axj&#xff1a;查看守护进程 3.进程组&#xff1a;多个进程的集合&#xff0c;由于管理…

Flask之模板

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 目录 一、模板的基本用法 1.1、创建模板 1.2、模板语法 1.3、渲染模板 二、模板辅助工具 2.1、上下文 2.2、全局对象 2.3、过滤器 2.4、测试…

小米测开二面—80min中核

小米测开二面—80min中核 3.28 无自我介绍直接开问&#xff01;你的第一份实习是一个开发工作你的第二实习为什么又跑到测试了你的第一份实习遇到了哪些挑战你的逆向开发的开发目标是什么&#xff0c;使用了什么工具你最终开发落地是用在了什么方面上&#xff0c;比如机器人路…

什么是云主机?

云主机是新一代的主机租借服务&#xff0c;它整合了高性能服务器与优质网络带宽&#xff0c;有用处理了传统主机租借价格偏高、服务品良莠不齐等缺陷&#xff0c;可全面满意中小企业、个人站长用户对主机租借服务低本钱&#xff0c;高牢靠&#xff0c;易办理的需求。   关于大…

C语言入门系列:可迁移的数据类型

文章目录 1&#xff0c;精确宽度类型(exact-width integer type)2&#xff0c;最小宽度类型&#xff08;minimum width type&#xff09;3&#xff0c;最快的最小宽度类型&#xff08;fast minimum width type&#xff09;4&#xff0c;可以保存指针的整数类型。5&#xff0c; …

编译 CanMV 固件

前言 上一章节中已经搭建好了基于 CanMV 的 C 开发环境&#xff0c;这么一来便可以进行基于 C 语言和 FreeRTOS 的应用开发或者编译基于 MicroPython 语法的应用开发方式所需的 CanMV 固件&#xff0c;本 章就将带领读者体验一下 CanMV 固件的编译流程。 本章分为如下几个小节&…

为什么print语句被Python3遗弃?

在开发和维护python项目的时候发现经常有print语句报错&#xff0c;原因是python3放弃了print语句 print 语句 早就被列在了不可靠的语言特性列表中&#xff0c;例如 Guido 的“Python 之悔”&#xff08;Python Regrets&#xff09;演讲【1】&#xff0c;并计划在 Python 300…

Python期末复习:基础+数据结构

合法的标识符定义规则 以字母或下划线开头&#xff1a; 标识符必须以字母&#xff08;大写或小写&#xff09;或下划线 _ 开头。 后续字符可以是字母、数字或下划线&#xff1a; 后续字符可以是字母&#xff08;大写或小写&#xff09;、数字&#xff08;0-9&#xff09…

Unity 从0开始编写一个技能编辑器_02_Buff系统的Handler

BuffHandler可以是用于处理角色身上buff的Mono类&#xff0c;任何具备跟Buff交互的角色&#xff0c;都要携带这个BuffHandler脚本。如果你的Buff有额外的处理机制&#xff0c;比如互斥Buff&#xff08;如&#xff1a;免疫负面效果的霸体&#xff09;&#xff0c;需要在AddBuff方…

Anthropic AI模型Claude 3.5 Sonnet在Amazon Bedrock上正式可用

Claude 3.5 Sonnet是Anthropic最先进的Claude系列AI模型的新成员&#xff0c;比Claude 3 Opus更智能且价格只有其五分之一 北京——2024年6月21日 亚马逊云科技宣布&#xff0c;Anthropic最新、最强大的模型Claude 3.5 Sonnet现已在Amazon Bedrock上正式可用&#xff0c;该模型…