【数据结构与算法】优先级队列(堆)

目 录

  • 一.优先级队列
    • 1.1 概念
  • 二.优先级队列的模拟实现
    • 2.1 堆的概念
    • 2.2 堆的存储方式
    • 2.3 堆的创建
      • 2.3.1 堆向下调整
      • 2.3.2 堆的创建
      • 2.3.3 建堆的时间复杂度
    • 2.4 堆的插入与删除
      • 2.4.1 堆的插入
      • 2.4.2 堆的删除
      • 2.4.3 获取堆顶元素
  • 三.常用接口介绍
    • 3.1.1 PriorityQueue 的特性
    • 3.1.2 PriorityQueue常用接口介绍
    • 3.3 优先级队列的应用
      • top-k 问题:最大或者最小的前k个数据。
    • 4.2 堆排序

一.优先级队列

1.1 概念

队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。

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


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

JDK1.8中的 PriorityQueue 底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。

2.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

在这里插入图片描述

2.2 堆的存储方式

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

在这里插入图片描述

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

将元素存储到数组中后,可以根据二叉树性质对树进行还原。假设 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 堆向下调整

  1. 让 parent 标记需要调整的节点,child 标记 parent 的左孩子(注意:parent 如果有孩子一定先是有左孩子)
  2. 如果 parent 的左孩子存在,即: child < size, 进行以下操作,直到parent 的左孩子不存在
  • parent 右孩子是否存在,存在找到左右孩子中最大的孩子,让child进行标
  • 将 parent 与较大的孩子child比较,如果:
    parent 大于较大的孩子child,调整结束,否则:交换 parent 与较大的孩子child,交换完成之后,parent 中小的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即 parent = child;child = parent*2+1; 然后继续2。

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

时间复杂度分析:

最坏的情况即图示的情况,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为 Olog2(n)

2.3.2 堆的创建

对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?

  1. 从最后一棵子树开始调整

  2. 每棵子树调整的时候,都是向下调整。

  1. 如何确定最后一棵子树的根节点。
    p = (len-1-1)/2->是不是就是最后一棵子树的根节点 ! !
  2. 调整完一棵树之后,怎么到下一棵树?
    p - -
  3. 直到调整完 0 下标这棵树就好了。
public static void main1(String[] args) {TestHeap testHeap = new TestHeap();int[] array = { 27,15,19,18,28,34,65,49,25,37 };testHeap.createHeap(array);}
public class TestHeap {public int[] elem;public int usedSize;public TestHeap() {this.elem = new int[10];}/*** 建堆的时间复杂度:*/public void createHeap(int[] array) {//这一步不算是必须的。这里只是我们准备数据,不算做我的建堆时间复杂度当中for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}for (int p = (usedSize-1-1)/2; p >= 0 ; p--) {shiftDown(p,usedSize);}}/*** @param root 是每棵子树的根节点的下标* @param len  是每棵子树调整结束的结束条件* 向下调整的时间复杂度:O(logn)*/private void shiftDown(int root,int len) {int parent = root;int child = 2*parent+1;//进入这个循环,说明一定至少有一个孩子while (child < len) {//如果有孩子,找到左右孩子的最大值if(child+1 < len && elem[child] < elem[child+1]) {child++;}//child下标一定保存的是左右孩子最大值的下标//接下来,孩子的最大值和根节点去比较大小if(elem[child] > elem[parent]) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;parent = child;//开始更新下标,继续看下面的子树是不是大根堆child = 2*parent+1;}else {break;//此时说明已经是大根堆,不需要进行再次调整了}}}
}

2.3.3 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)

在这里插入图片描述

因此:建堆的时间复杂度为O(N)。


2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入总共需要两个步骤:

  1. 先将元素放入到底层空间中(注意:空间不够时需要扩容)
  2. 将最后新插入的节点向上调整,直到满足堆的性质

在这里插入图片描述

  1. 从最后一棵子树开始向上调整,下标位置上面讲过
  2. 直到c == 0 或者 p<0
/*** 入队:仍然要保持是大根堆* @param val*/
public void push(int val) {if(isFull()) {elem = Arrays.copyOf(elem,2*elem.length);}//1、放到最后的位置elem[usedSize] = val;//2、进行向上调整shiftUp(usedSize);//3、有效数据+1usedSize++;
}private void shiftUp(int child) {int parent = (child-1) / 2;while (child > 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 usedSize == elem.length;
}

2.4.2 堆的删除

注意:堆的删除一定删除的是堆顶元素。具体如下:

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整
/*** 出队【删除】:每次删除的都是优先级高的元素* 仍然要保持是大根堆*/
public void pollHeap() {if(isEmpty()) {System.out.println("优先级队列为空!");return;}int tmp = elem[0];elem[0] = elem[usedSize-1];elem[usedSize-1] = tmp;usedSize--;//9shiftDown(0,usedSize);
}public boolean isEmpty() {return usedSize == 0;
}

2.4.3 获取堆顶元素

/*** 获取堆顶元素* @return*/
public int peekHeap() {if(isEmpty()) {System.out.println("优先级队列为空!");return -1;}return elem[0];
}

三.常用接口介绍

3.1.1 PriorityQueue 的特性

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

关于 PriorityQueue 的使用要注意:

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

3.1.2 PriorityQueue常用接口介绍

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

注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器

在这里插入图片描述

可以看到我们有如上两种方法去设置大小堆

  1. 插入/删除/获取优先级最高的元素
函数名功能介绍
boolean offer(E e)插入元素e,插入成功返回true,如果e对象为空,抛NullPointerException异常,时间复杂度 O(log2(N)) ,注意:空间不够时候会进行扩容
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()获取有效元素的个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空,空返回true

在这里插入图片描述

优先级队列的扩容说明:

  • 如果容量小于64时,是按照 oldCapacity 的2倍方式扩容的
  • 如果容量大于等于64,是按照 oldCapacity 的1.5倍方式扩容的
  • 如果容量超过 MAX_ARRAY_SIZE,按照 MAX_ARRAY_SIZE 来进行扩容

3.3 优先级队列的应用

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

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决

思路一:(例如前三个最大元素)

1、对整个数组进行排序,然后取前10个元素。
2、借助堆来进行操作前3个最大的
2.1 先将整体元素,建成最大堆
2.2 出队3次,这3个就是最大的

/*** 时间复杂度:O(n*logn)* @param array* @param k* @return*/
public static int[] topK1(int[] array,int k) {IntCmp intCmp = new IntCmp();PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(intCmp);//n*lognfor (int i = 0; i < array.length; i++) {priorityQueue.offer(array[i]);//插入堆中}//大堆创建完毕   n*lognint[] ret = new int[k];for (int i = 0; i < k; i++) {int val = priorityQueue.poll();ret[i] = val;}return ret;
}

思路二:(常用,推荐)

  1. 用数据集合中前K个元素来建堆
  • 前k个最大的元素,则建小堆
  • 前k个最小的元素,则建大堆
  1. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

1、先将数组前K个元素建成小根堆
2、从数组的第K+1个元素开始和堆顶元素比较。
3、如果当前i下标的元素,大于当前堆顶的值,那么堆顶元素出队,然后将i下标的值入堆。
4、直到遍历完整个数组,结束了!

/*** 求最小的 K个数* @param array  N*logk* @param k* @return*/
public static int[] topK(int[] array,int k) {IntCmp intCmp = new IntCmp();PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return o2-o1;}});for (int i = 0; i < array.length; i++) {if(maxHeap.size() < k) {maxHeap.offer(array[i]);}else {int top = maxHeap.peek();if(array[i] < top) {maxHeap.poll();maxHeap.offer(array[i]);}}}int[] ret = new int[k];for (int i = 0; i < k; i++) {int val = maxHeap.poll();ret[i] = val;}return ret;
}

1. 第K大的元素怎么求?

小根堆顶元素就是

2. 第K小的元素怎么求?

大根堆顶元素就是


4.2 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

从小到大排序:建大根堆

  • 过程:
    1、建立大根堆
    2、0下标和end下标交换
    3、调整0下标这棵树 shiftDown(0,end)
    4、end–

从大到小排序:建小根堆

例如从小到大排序

//O(n*logn)
public void heapSort() {int end = usedSize-1;while (end > 0) {int tmp = elem[0];elem[0] = elem[end];elem[end] = tmp;shiftDown(0,end);end--;}
}

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

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

相关文章

在centOS服务器安装docker,并使用docker配置nacos

遇到安装慢的情况可以优先选择阿里镜像 安装docker 更新yum版本 yum update安装所需软件包 yum install -y yum-utils device-mapper-persistent-data lvm2添加Docker仓库 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.rep…

HSE化工应急安全生产管理平台:衢州某巨大型化工企业的成功应用

在化工行业中&#xff0c;安全生产一直是至关重要的议题。为了提高生产安全性、降低成本并提升企业形象&#xff0c;衢州某巨大型化工企业引入了HSE化工应急安全生产管理平台&#xff0c;取得了显著的改善和获益。 该平台的核心功能包括风险管理和应急预案制定。通过对化工生产…

基于深度学习的图像去雨去雾

基于深度学习的图像去雨去雾 文末附有源码下载地址 b站视频地址&#xff1a; https://www.bilibili.com/video/BV1Jr421p7cT/ 基于深度学习的图像去雨去雾&#xff0c;使用的网络为unet&#xff0c; 网络代码&#xff1a; import torch import torch.nn as nn from torchsumm…

Day32:安全开发-JavaEE应用Servlet路由技术JDBCMybatis数据库生命周期

目录 JavaEE-HTTP-Servlet&路由&周期 JavaEE-数据库-JDBC&Mybatis&库 思维导图 Java知识点&#xff1a; 功能&#xff1a;数据库操作&#xff0c;文件操作&#xff0c;序列化数据&#xff0c;身份验证&#xff0c;框架开发&#xff0c;第三方库使用等. 框架…

使用OCC进行旋转扫掠

旋转扫掠是将物体以某一个坐标轴为参照&#xff0c;按照指定的角度旋转生成新的图形的过程 这里使用面的案例&#xff0c;使用线的逻辑处理其实是一样的 //构造旋转轴 gp_Ax1 anAxis; //设置轴的原点 anAxis.SetLocation(0,0,0); //设置轴的方向 anAxis.SetDirection(gp_Dir(0…

如何处理Android悬浮弹窗双击返回事件?

目录 1 前言 1.1 准备知识 1.2 问题概述 2 解决方案 3 代码部分 3.1 动态更新窗口焦点 3.2 窗口监听返回事件 3.3 判断焦点是否在窗口内部 3.4 窗口监听焦点移入/移出 1 前言 1.1 准备知识 1&#xff09;开发环境&#xff1a; 2D开发环境&#xff1a;所有界面或弹窗…

Vue3+.NET6前后端分离式权限管理系统完整项目实战

1&#xff0c;前景&#xff1a;学习和开发该项目的需要一定的HTMLCSS基础&#xff0c;知道基本的Dom结构和标签使用以及简单的样式&#xff0c;此外如果有后端基础的想学前端的话比较容易上手&#xff0c;项目比较简单&#xff01; 后续更新主要在微信订阅号更新&#xff0c;手…

APPS数据集分享

来源: AINLPer公众号&#xff08;每日干货分享&#xff01;&#xff01;&#xff09; 编辑: ShuYini 校稿: ShuYini 时间: 2024-3-13 该数据集由纽约大学的研究者于2022年提出&#xff0c;它是一个创新的多项选择题数据集&#xff0c;旨在提升自然语言理解模型处理长文本的能力…

【2024 R1 版本更新】Ansys Fluent(上)

​​Ansys2024R1来了&#xff0c;小宇赶紧将新功能给大家汇报一下。GPU求解器功能势头最强&#xff0c;pyFluent又开始迭代了&#xff0c;CPU模型中又更新了很多功能&#xff0c;fluent meshing中的thin volume mesh功能也来了。

环保企业应适应行业发展趋势,不断创新和提升竞争力|中联环保圈

《2023年行业评述及2024年发展展望》一文&#xff0c;由中国环保产业协会撰写&#xff0c;全面审视了过去一年我国生态环保产业的发展状况&#xff0c;并对新的一年发展趋势进行了深度预测。该报告以行业主要政策标准为基础&#xff0c;结合报告以及新冠疫情防控转段后的经济恢…

ARMV8-aarch64的虚拟内存(mmutlbcache)介绍-概念扫盲

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 思考: 1、cache的entry里都是有什么&#xff1f; 2、TLB的entry里都是有什么? 3、MMU操作…

Mysql锁与MVCC

文章目录 Mysql锁的类型锁使用MVCC快照读和当前读读视图【Read View】串行化的解决 exlpain字段解析ACID的原理日志引擎整合SpringBoot博客记录 Mysql锁的类型 MySQL中有哪些锁&#xff1a; 乐观锁&#xff08;Optimistic Locking&#xff09;&#xff1a;假设并发操作时不会发…