【数据结构】优先级队列 — 堆

news2024/9/23 9:29:54

img

文章目录

  • 前言
  • 1. 优先级队列
    • 1.1 概念
    • 1.2 特性
  • 2. 堆
    • 2.1 概念
    • 2.2 存储方式
  • 3. 堆的模拟实现
    • 3.1 堆的创建
    • 3.2 堆的插入
    • 3.3 堆的删除
  • 4. PriorityQueue
    • 4.1 注意事项
    • 4.2 构造器介绍
    • 4.3 常用方法介绍
  • 5. 经典题型
  • 6. 结语


前言

我们之前学习过队列,它是遵循先进先出原则的数据结构,对应我们现实生活中的先到先得原则,比如排队时我们就可以使用队列的数据结构来模拟实现。但是,如果我们想优先操作队列中的某些数据,即让优先级高的元素先出队列,那么这种“普通的”先进先出队列就无法满足我们的需求。这时候就需要“优先级队列”来帮忙了


1. 优先级队列

1.1 概念

优先级队列(Priority Queue)是一种抽象数据类型,它类似于常规的队列或栈,但每个元素都有一个优先级。在优先级队列中,最小元素(或最大元素,根据实现)总是在队列的前端,并且最先被移除。它通常用于需要根据元素的重要性或紧急性进行排序的场景。比如:任务调度,网络数据传输,订单处理等等


1.2 特性

  1. PriorityQueue 类是实现优先级队列的常用类,底层实现是,而堆一棵特殊的完全二叉树(下面我们会重点讲解堆)
  2. 默认情况下,PriorityQueue 不允许插入重复的元素
  3. PriorityQueue 不是线程安全的。如果需要在多线程环境中使用,我们可以使用 PriorityBlockingQueue,它是线程安全的优先级队列实现

2. 堆

2.1 概念

在逻辑结构上,堆是一棵完全二叉树,而在存储结构上,堆是通过一个一维数组来存储元素的。它把所有元素按完全二叉树的层序遍历顺序,存储在一个数组当中。并且在堆中,每个节点都必须满足以下两种属性之一:

小根堆(最小堆):父节点的键值必须小于或等于其子节点的键值

大根堆(最大堆):父节点的键值必须大于或等于其子节点的键值

image-20240728160559698


2.2 存储方式

在上面我们提到了,“堆是通过一个一维数组来存储元素的”,而它的存储顺序就是按照完全二叉树的层序遍历来存储的,这样可以更高效的利用存储空间

因为使用数组存储堆可以方便地实现父节点到子节点的转换,所以我们可以给每一个节点记一个下标,通过下标来得到节点之间的父子关系:

我们规定根节点的下标为 0,设 i 为节点在数组中的下标,则有:

  • 父节点的下标:(i - 1) / 2
  • 左节点的下标: i * 2 + 1(如果得出来的数大于等于总的节点个数,则该节点没有左节点)
  • 右节点的下标: i * 2 + 2(如果得出来的数大于等于总的节点个数,则该节点没有右节点)

下面是简单的对应关系图:

image-20240728173958870


3. 堆的模拟实现

3.1 堆的创建

问题:把集合 {37,26,77,45,6,21,66,18,42} 创建成一个大根堆

思路:我们可以先把集合中的数据先按原始顺序排成一棵完全二叉树,接着再来调整,直至调整成为一个大根堆

image-20240804151549785

那要怎么调整呢?很简单,我们回忆一下大根堆的特点:在每棵子树中,父节点始终大于子节点。顺着这个思路,我们可以得出方法:从最后一棵子树开始调整,拿根节点和两个子节点比大小,如果出现任一子节点大于根节点,就交换两者的位置,这个过程叫做“根节点的向下调整”;然后接着往前找子树,一样的套路继续调整,直至所有的父节点都大于子节点,最后调整结束,得到的就是大根堆了

干讲可能有点抽象,我们根据代码来辅助理解:

首先我们创建一个数组 elem,并设定初始容量为 10,再使用 usedSize 来记录实际容量;接着初始化数组,把要传进去的数组赋值进去

public class MyHeap {

    public int[] elem;
    public int usedSize;

    //构造方法
    public MyHeap() {
        this.elem = new int[10];
    }

    //初始化,把数组一个个赋值进去
    public void init(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }
}

现在我们就得到了一组原始数组,它是一棵待排序的二叉树,接着开始调整

    //把 elem 数组中的数据调整为大根堆
    //时间复杂度为 O(n)
    public void createHeap() {
        //parent 为待调整树的父节点位置
        for (int parent = (usedSize - 1 - 1) / 2; parent >= 0 ; parent--) {
            shiftDown(parent, usedSize);
        }
    }

    //向下调整
    public void shiftDown(int parent, int end) {
        int child = 2 * parent + 1;
      	//如果存在孩子节点,就进入循环
        while (child < end) {
            //判断最大的孩子是在左还是在右
            if (child + 1 < end && elem[child] < elem[child+1]) {
                child++;
            }
           //此时child一定是最大的孩子
            if (elem[child] > elem[parent]) {
                swap(child, parent);
                //交换完后再往下走
                parent = child;
                child = 2 * parent + 1;
            }
            else {
                //说明不需要调整,直接跳出
                break;
            }
        }
    }

    //交换位置
    public void swap(int i, int j) {
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }

通过这么一套操作,我们就能得到大根堆,最后打印出来就是顺序就是正确的

image-20240804160842761

注意:在调整以 parent 为根的树时,要保证它的左右子树都已经是堆了才能向下调整,这也是我们从最后一棵子树开始调整的原因


时间复杂度:在最坏的情况中,我们需要对每个元素进行下沉操作,下沉操作的时间复杂度是 O(log n),因为堆是一个完全二叉树,其高度是 log(n)。但总的下沉次数是 n,因此平均到每个节点上的时间复杂度就是常数时间。这导致总的时间复杂度是 O(n)


3.2 堆的插入

将元素插入到堆中,插入后依然要满足堆的性质

实现思路

  1. 判断数组容量是否满了,满的话要扩容
  2. 把要插入的数据放在最后面,记得让 usedSize++,接着向上调整 (siftUp)
  3. 直到满足堆的性质

在原先的大根堆中插入 54,要求最后的结果还是大根堆

    //堆的插入
    public void offer(int val) {
        if (isFull()) {
            //满了就扩容
            elem = Arrays.copyOf(elem,2 * elem.length);
        }
        //在最后一个位置添加欲插入数据
        elem[usedSize] = val;
        usedSize++;
        //向上调整
        shiftUp(usedSize - 1);
    }

    //向上调整
    public void shiftUp(int child) {
        //先找到父节点
        int parent = (child - 1) / 2;
        //如果存在父节点,就进入循环
        while (parent >= 0) {
            if (elem[child] > elem[parent]) {
                swap(child, parent);
                //交换后,继续往上,看是否需要调整
                child = parent;
                parent = (child - 1) / 2;
            } else {
                break;
            }
        }
    }

	//判满
    public boolean isFull() {
        return usedSize == elem.length;
    }

image-20240804164034279


3.3 堆的删除

因为堆的特性,所以堆的删除我们通常指的是堆顶元素的删除,也就是说删除的是堆中最大(或者最小)的元素

实现思路:目的是为了尽可能的减少堆中元素的移动,保持堆的特性,因此我们可以采用逻辑删除

  1. 让堆顶元素和堆中最后一个元素交换
  2. 让 usedSize 减一(逻辑删除)
  3. 最后调整堆顶元素,向下调整

image-20240807175209051


删除堆中的堆顶元素,并要求删除后依然是一个大根堆

    //弹出第一个数字
    public int poll() {
        //先判断堆是不是空的
        if (isEmpty()) {
            return -1;
        }

        int old = elem[0];//记录下第一个数
        //把第一个和最后一个交换
        swap(0,usedSize - 1);
        //再排成大根堆
        usedSize--;//把最后一个数覆盖掉
        //第一个数向下调整,传入自己位置和堆的实际容量
        shiftDown(0,usedSize);
        //返回删除的元素
        return old;
    }

    public boolean isEmpty() {
        return usedSize == 0;
    }

除了 poll( ) 弹出,我们也可以来实现 peek( ),即瞥一眼堆顶元素,这个就很简单了

    //瞥一眼第一个元素
    public int peek() {
        //先判断堆是不是空的
        if (isEmpty()) {
            return -1;
        }
        return elem[0];
    }

4. PriorityQueue

4.1 注意事项

在使用之前,记得导包

import java.util.PriorityQueue;

接下来,我们来详细讲解使用时的一些注意点:

  1. 在 PriorityQueue 中放置的元素必须是可比较的,若插入无法比较大小的元素,就回抛出 ClassCastException 类型转换异常
  2. 在使用优先队列之前,应检查队列是否为空,避免在空队列上执行删除操作导致错误
  3. 不能插入 null,否则会抛出 NullPointerException 空指针异常
  4. PriorityQueue 默认情况下是小堆,若想建成的是大堆,则需要在构造时传入比较器
  5. PriorityQueue 不是线程安全的。如果需要在多线程环境中使用,我们可以使用 PriorityBlockingQueue,它是线程安全的优先级队列实现

4.2 构造器介绍

  1. 默认构造方法:创建一个空的优先队列,默认容量为 11

    public PriorityQueue() 
    
  2. 带初始容量的构造方法:可以指定初始容量

    public PriorityQueue(int initialCapacity)
    
  3. 带初始集合的构造方法:可以指定集合中的元素来初始化(实现了 Collection 接口的就可以接收,该集合的类型是包含 E 类型或其任何子类型的对象)

    public PriorityQueue(Collection<? extends E> c)
    
  4. 带比较器的构造方法:可以使用指定的比较器来初始化(比较器决定了元素的排序方式)

    public PriorityQueue(Comparator<? super E> comparator)
    
  5. 带初始集合和比较器的构造方法:可以使用指定集合中的元素和比较器来初始化

    public PriorityQueue(Collection<? extends E> c, Comparator<? super E> comparator)
    

在这里我们重点演示一下带比较器的构造方法:

class IntCmp implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1); //调换了原来的顺序
    }
}
public class Test {
    public static void main(String[] args) {
        //不传入比较器
        PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();
        priorityQueue1.offer(24);
        priorityQueue1.offer(52);
        priorityQueue1.offer(14);
        priorityQueue1.offer(37);
        System.out.println("小根堆:" + priorityQueue1);

        //传入比较器
        PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(new IntCmp());
        priorityQueue2.offer(24);
        priorityQueue2.offer(52);
        priorityQueue2.offer(14);
        priorityQueue2.offer(37);
        System.out.println("大根堆:" + priorityQueue2);
    }
}    

运行结果如下:

image-20240808170726831


4.3 常用方法介绍

方法描述
boolean offer(E e)将指定元素添加到此队列中
E peek( )返回队列头部的元素但不移除它。如果队列为空,则返回 null
E poll( )移除此队列头部的元素。如果队列为空,则返回 null
int size( )返回队列中的元素数量
boolean remove(Object o)从队列中移除指定的元素,移除成功返回 true,否则返回 false
void clear( )移除队列中的所有元素
boolean isEmpty( )如果队列为空,则返回 true

以上都是 PriorityQueue 的常用方法,有需要的话可以去该网址查询: PriorityQueue 的官方文档


5. 经典题型

Top-K 问题:求数据集合中前 K 个大(或者小)的元素

我们可以想想要怎么解决这个问题?最“朴素”的方法,当然是直接把数据集合排序,这样取到前 K 个大(或者小)的元素当然很简单

但是如果此时的数据量非常大,用排序法的时间复杂度一定不小,因此我们可以使用来求,那要怎么求呢?


力扣:最小K个数

image-20240814170551181


思路:问题要求我们找到前 k 个小的元素,“前 K 个”的意思就是我们不需要给所有数排序,只要把数据集合中的前 k 个小的元素就行

题目要求取出数据中前 k 个小的元素,我们就得建大堆(有点反直觉),把前 k 个数据先建成一个大堆,接着将剩余的 N - k 个元素依次和堆顶元素比较,比堆顶元素小就换,比完后堆中剩余的 k 个元素就是题目所要求的数

class IntCmp implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1); 
    }
}

class Solution {
    public int[] smallestK(int[] arr, int k) {
        //建大堆,只放前k个数
        PriorityQueue<Integer> minK = new PriorityQueue<>(new IntCmp());
        for (int i = 0; i < k; i++) {
            minK.offer(arr[i]);
        }
        
        //比较大小,若比堆顶元素小就放进去
        for (int i = k; i < arr.length; i++) {
            if(minK.size() != 0 && arr[i] < minK.peek()) {
                minK.poll();
                minK.offer(arr[i]);
            }
        }

        //返回
        int[] ret = new int[k];
        for (int i = 0; i < k; i++) {
            ret[i] = minK.poll();
        }
        return ret;
    }
}

当然,有很多其他更好的解法,博主在这只是提供一个简单的思路


6. 结语

今天我们介绍了优先级队列,重点掌握它的特性,还有堆的概念以及模拟实现,明白怎么使用 PriorityQueue 的常用方法,还有 Top—K 问题,也是非常经典~

希望大家能够喜欢本篇博客,有总结不到位的地方还请多多谅解。若有纰漏,希望大佬们能够在私信或评论区指正,博主会及时改正,共同进步!

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

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

相关文章

云上Oracle 数据库本地备份部署测试

1.说明 由于运行在云上的Oracle数据库暂无本地备份&#xff0c;为了保障租户业务系统的可持续性以及数据安全&#xff0c;特此进行数据库备份本地部署并进行测试。 2.备份策略 &#xff08;1&#xff09;数据库数据量 SQL> select sum(bytes)/1024/1024/1024 from dba_segme…

自建电商网站整合Refersion教程

前言&#xff1a;   先介绍一下Refersion有啥用&#xff0c;如果你有一个自己的跨境电商独立站点&#xff0c;想找一些网红帮忙推广销售自己的商品&#xff0c;然后按照转化订单比例给网红支付佣金&#xff0c;这件事情对双方来说透明性和实时性很重要&#xff0c;Refersion就…

《多模态大规模语言模型基准》综述

论文链接&#xff1a;https://arxiv.org/pdf/2408.08632 MLLM&#xff1a;Multimodal Large Language Models 评估多模态大型语言模型&#xff08;MLLMs&#xff09;的重要性体现在以下几个方面&#xff1a; 1. 理解模型能力&#xff1a;通过评估&#xff0c;研究人员和开发…

攀高行为检测识别摄像机

攀高行为检测识别摄像机 是一种结合了图像识别技术和智能算法的设备&#xff0c;旨在监测和识别人员在高空作业中的攀高行为&#xff0c;及时发现潜在的安全隐患并提供预警。这种摄像机可以有效提高工作场所的安全管理水平&#xff0c;减少高空作业事故的发生。 攀高行为检测识…

微软CEO谈AI平台范式转移、AI发展趋势及资本市场动态

为大家整理编辑了近期微软CEO萨提亚纳德拉 (Satya Nadella)著名科技KOLBen Thompson播客采访的精华内容。 在采访中&#xff0c;萨提亚纳德拉 (Satya Nadella)畅谈了人工智能平台范式转移、与Open AI等合作伙伴的关系、AI未来五年的发展方向、以及资本市场的趋势。 萨提亚纳德…

VUE3生命周期钩子

生命周期 vue2的生命周期钩子 beforeCreate&#xff1a;开始初始化事件和生命周期&#xff0c;但还没有data、methods、computed、watch属性&#xff0c;也就是vue实例的挂载元素$el和数据对象data都为undefined&#xff0c;还未初始化。 created&#xff1a;实完成数据挂载、…

PowerBi 柱形图,数据标签无法显示在端外

如图 即使设置了“数据标签”显示“端外“&#xff0c;仍然不作用。 原因其实是因为Y轴的数据范围设置不当&#xff0c;如图&#xff0c;当前Y轴范围是0到自动 只需要修改为最大和最小值都是自动即可&#xff0c;选中0 按backspace键删除&#xff0c;然后&#xff0c;鼠标在任意…

Parade Series - 3D Modeling

FBX FBX&#xff08;Filmbox&#xff09;文件格式是一种广泛使用的三维模型和动画文件格式&#xff0c;由Autodesk开发和维护。 FBX格式支持多种3D数据类型&#xff0c;包括几何、材质、纹理、动画、骨骼、灯光和摄像机等;OBJ MTL OBJ文件格式是一种用于表示三维几何形状的标…

OpenAI 神秘模型「草莓」预计今秋推出,ChatGPT 将迎重大升级|TodayAI

有外媒报道指出&#xff0c;OpenAI 内部代号为「Strawberry&#xff08;草莓&#xff09;」的 AI 模型即将在今年秋季面世。这一消息引发了业内广泛关注&#xff0c;被认为可能会为 ChatGPT 带来今年最重要的升级。 「草莓」模型的强大能力与应用潜力 据《The Information》报…

EPLAN中绘制黑盒的具体方法

EPLAN中绘制黑盒的具体方法 对于某些电气元件没有EDZ部件库时,可以自己绘制黑盒来解决,具体方法可参考以下内容: 如下图所示,打开EPLAN软件,在项目中新建一页多线原理图, 如下图所示,点击插入----盒子/连接点/安装板--------黑盒, 设置所需的参数和属性,然后放置在图框绘制…

算法入门-深度优先搜索1

第六部分&#xff1a;深度优先搜索 144.二叉树的前序遍历&#xff08;简单&#xff09; 题目&#xff1a;给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3] 第一种思路&am…

AtCoder Beginner Contest 366(D~E题解)

闲来无事去vp了一下之前放假没打的比赛&#xff0c;感觉需要总结的也就这两题吧&#xff0c;a&#xff0c;c都是水题&#xff0c;b只不过是实现有一点难&#xff0c;并不是很难写&#xff0c;d是一个需要自己推的三维前缀和&#xff0c;e也是一种前缀和&#xff0c;我当时没想到…

WEB渗透Win提权篇-白名单提权

提权工具合集包&#xff08;免费分享&#xff09;&#xff1a; 夸克网盘分享 往期文章 WEB渗透Win提权篇-提权工具合集-CSDN博客 WEB渗透Win提权篇-RDP&Firewall-CSDN博客 WEB渗透Win提权篇-MSSQL-CSDN博客 WEB渗透Win提权篇-MYSQL-udf-CSDN博客 WEB渗透Win提权篇-Acc…

什么是代码审查(Code Review)?它有什么好处?

代码审查&#xff08;Code Review&#xff09;是软件开发过程中一个至关重要的环节&#xff0c;它指的是团队成员之间相互检查、评估代码的过程。这一过程不仅涉及对代码质量的把控&#xff0c;更是提升团队整体编程能力、确保软件安全性的重要手段。在本文中&#xff0c;我们将…

CSRF 概念及防护机制

概述 CSRF&#xff08;Cross-Site Request Forgery&#xff09;&#xff0c;即跨站请求伪造&#xff0c;是一种网络攻击方式。在这种攻击中&#xff0c;恶意用户诱导受害者在不知情的情况下执行某些操作&#xff0c;通常是利用受害者已经登录的身份&#xff0c;向受害者信任的…

我是如何在一分钟之内,不用多次交互AI,就完成了指定任务

本文背景 为什么我的AI不听话&#xff1f; 为什么我用AI写知乎文、视频文案、豆瓣影评、工作日报、论文、商业策划案、标书、代码都一直得不到想要的效果&#xff1f; 为什么我的AI生成的都是没有价值的口水文&#xff1f; 大象经过大量的AI实战经验&#xff0c;给出了这些问题…

ESP32-C3在MQTT访问时出现“Last error code reported from esp-tls: 0x8001”和问题的分析(3)

接前一篇文章:ESP32-C3在MQTT访问时出现“Last error code reported from esp-tls: 0x8001”和问题的分析(2) 上一回讲解了所遇问题log中的“esp-tls: couldnt get hostname for :iot-emqx-pre.nanshe-tech.com: getaddrinfo() returns 202, addrinfo=0x0”,再来回顾一下。…

USB:物理接口

USB&#xff1a;物理接口 物理接口 从高级概述角度来看&#xff0c;USB 的物理接口具有两个组件&#xff1a;线缆和连接器。这些连接器将设备连接到主机上。 一个 USB 线缆包含由一个绝缘套保护的多个组件。该绝缘套下面是一个包含了一个带有铜面的外部扩展板。 外部扩展板内包…

为什么现在人工智能大部分都用python而不是其他软件呢?

大部分人都选择使用Python而不是其他软件&#xff0c;主要是因为Python具有多方面的优势&#xff0c;这些优势使其在众多编程语言中脱颖而出&#xff0c;成为许多领域&#xff0c;特别是IT和人工智能领域的首选。以下是几个主要原因&#xff1a; 1. 简单易学 Python的语法简洁…

PMP备考3A的心得分享

首先&#xff0c;每一位报考的都会收到一份学习计划表&#xff0c;一定要仔细阅读这张表&#xff0c;并与自己的时间结合起来&#xff0c;看是否会有很大的冲突&#xff0c;如果有&#xff0c;那么可以找老师帮忙解决。一般来说&#xff0c;学习计划表的时间安排是非常恰当的&a…