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

news2025/1/13 9:40:46

目录

  • 一、什么是优先级队列?
  • 二、堆 (heap,基于二叉树)
    • 2.1 什么是堆?
    • 2.2 堆的分类
    • 2.3 结构与存储
  • 三、堆的操作
    • 3.1 堆创建
    • 3.2 插入元素
    • 3.3 弹出元素
  • 四、用堆模拟实现优先级队列
  • 五、堆的一个重要应用-堆排序
  • 六、经典的TOPK问题
    • 6.1 排序
    • 6.2 堆

一、什么是优先级队列?

如果我们给每个元素都分配一个数字来标记其优先级,不妨设较小的数字具有较高的优先级,这样我们就可以在一个集合中访问优先级最高的元素并对其进行查找和删除操作了。这样,我们就引入了 优先级队列 这种数据结构。 优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有(1)查找(2)插入一个新元素 (3)删除 一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。–(来源百度)

  1. 普通队列:FIFO。按照元素的入队顺序出队,先入先出。
  2. 按照优先级的大小动态出队(动态指的是元素个数动态变化,而非固定)。

举个现实里面的例子:
排队看病,如果病情相同的情况下会按照先来先进,如果病情严重,优先会看病。
电脑内存占用的资源是有限的,当资源不够的时候,会优先让优先级高的应用占用资源。

二、堆 (heap,基于二叉树)

2.1 什么是堆?

堆在逻辑上是一颗完全二叉树(不存储空节点值),也可以叫二叉堆(Binary Heap)

堆总是攒足下列性质:

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

堆是非线性数据结构,相当于一维数组,有两个直接后继。

2.2 堆的分类

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

2.3 结构与存储

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
在这里插入图片描述
注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率比较低。

三、堆的操作

3.1 堆创建

创建堆之前我们需要复习一个知识点,那就是父子节点之间的关系,

将元素存储到数组中后,可以根据二叉树的性质对树进行还原。假设i为节点在数组中的下标,则有:
如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

复习了这个知识点之后,我们就可以开始思考怎么去创建堆了?
创建的方式其实有很多种,那么我们这里就采用大根堆方式来创建吧!

还记得大根堆是什么特性吗?
将根结点最大的堆叫做最大堆或大根堆。

根据大根堆的特性,我们创建堆的思路是:向下操作。

  1. 需要一个parent标记需要调整的节点。
  2. 设置一个循环条件,使得每次调整都能进行,直至停止。

我们可以用child来标记parent的左孩子,如果存在,判断右孩子是否存在,如果存在找到左右孩子中最大的,依然用child标记,将parent与大孩子比较,parent小于小孩子,交换。

public class TestHeap {
public int[] elem;
    public int usedSize;

    public TestHeap() {
        this.elem = new int[10];
    }

    /**
     * 向下调整函数的实现
     *
     * @param parent 每棵树的根节点
     * @param len    每棵树的调整的结束位置
     */
    public void shiftDown(int parent, int len) {
        int child = 2 * parent + 1;
        // 1. 至少有1个孩子
        while (child < len) {
            if (child + 1 < len && elem[child] < elem[child + 1]) {
                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;
            }
        }
    }

    public void createBigHeap(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }

        // 根据代码 显示的时间复杂度 看起来应该是O(N*logn) 但是实际上时O(N)
        for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
            // 调整
            shiftDown(parent, usedSize);
        }
    }
}

3.2 插入元素

插入元素之前,非常经典的我们会考虑到扩容的问题,我们需要判满之后扩容。
扩容之后我们就可以插入新元素了,为了保证堆的结构性,用该位置元素和父亲元素比较,如果大于父亲元素,则交换父子元素,然后指向父亲的位置。再与该位置的父亲位置元素比较,如果父亲元素大则重复上述操作,否则插入结束。

	public 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 void offer(int val) {
        if (isFull()) {
            // 扩容
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize++] = val;
        // 注意这里传入的是usedSize-1
        shiftUp(usedSize-1);
    }
    public boolean isFull() {
        return usedSize == elem.length;
    }

3.3 弹出元素

同样在弹出元素之前,我们需要判断是否为空!
如何弹出元素呢?
先将堆尾元素和堆首元素进行交换,然后将usedSize–;之后对堆首元素向下操作即可。

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

    public int poll() {
        if (isEmpty()) {
            throw new RuntimeException("优先级队列为空!");
        }
        int tmp = elem[0];
        elem[0] = elem[usedSize-1];
        elem[usedSize-1] = tmp;
        shiftDown(0, usedSize);
        return tmp;
    }

四、用堆模拟实现优先级队列

在这里插入图片描述
这里只模拟实现几个重要功能!

public class TestHeap {
    public int[] elem;
    public int usedSize;

    public TestHeap() {
        this.elem = new int[10];
    }

    /**
     * 向下调整函数的实现
     *
     * @param parent 每棵树的根节点
     * @param len    每棵树的调整的结束位置
     */
    public void shiftDown(int parent, int len) {
        int child = 2 * parent + 1;
        // 1. 至少有1个孩子
        while (child < len) {
            if (child + 1 < len && elem[child] < elem[child + 1]) {
                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;
            }
        }
    }

    public void createBigHeap(int[] array) {
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }

        // 根据代码 显示的时间复杂度 看起来应该是O(N*logn) 但是实际上时O(N)
        for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
            // 调整
            shiftDown(parent, usedSize);
        }
    }

    public void offer(int val) {
        if (isFull()) {
            // 扩容
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize++] = val;
        // 注意这里传入的是usedSize-1
        shiftUp(usedSize-1);
    }

    public 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;
    }

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

    public int poll() {
        if (isEmpty()) {
            throw new RuntimeException("优先级队列为空!");
        }
        int tmp = elem[0];
        elem[0] = elem[usedSize-1];
        elem[usedSize-1] = tmp;
        shiftDown(0, usedSize);
        return tmp;
    }

    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("优先级队列为空!");
        }

        return elem[0];
    }

}

五、堆的一个重要应用-堆排序

堆排序(、Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

  1. 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  2. 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
  3. 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

基本思想:

  1. 将待排序序列构造成一个大顶堆
  2. 此时,整个序列的最大值就是堆顶的根节点
  3. 将其与末尾元素进行交换,此时末尾就为最大值。
  4. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

1.将堆顶元素8和末尾元素3进行交换
在这里插入图片描述
2.重新调整结构,使其继续满足堆定义
在这里插入图片描述

3.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

	/**
     * 向下调整函数的实现
     *
     * @param parent 每棵树的根节点
     * @param len    每棵树的调整的结束位置
     */
    public void shiftDown(int parent, int len) {
        int child = 2 * parent + 1;
        // 1. 至少有1个孩子
        while (child < len) {
            if (child + 1 < len && elem[child] < elem[child + 1]) {
                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;
            }
        }
    }
	public void heapSort() {
        int end = this.usedSize-1;
        while (end > 0) {
            int tmp = elem[0];
            elem[0] = elem[end];
            elem[end] = tmp;
            shiftDown(0, end);
            end--;
        }
    }

六、经典的TOPK问题

设计一个算法,找出数组中最小的个数。以任意顺序返回这K个数。

  1. sort排序:取前K个元素
  2. 二叉搜索树:按照中序遍历回收K个数据
  3. 优先级队列:peek,poll

在这里插入图片描述

6.1 排序

对原数组从小到大排序后取出前 kk 个数即可。

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] vec = new int[k];
        Arrays.sort(arr);
        for (int i = 0; i < k; ++i) {
            vec[i] = arr[i];
        }
        return vec;
    }
}

  1. 时间复杂度:O(n*log n),其中 n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂度。
  2. 空间复杂度:O(logn),排序所需额外的空间复杂度为 O(logn)。

在这里插入图片描述

6.2 堆

我们用一个大根堆实时维护数组的前 kk 小值。首先将前 kk 个数插入大根堆中,随后从第 k+1k+1 个数开始遍历,如果当前遍历到的数比大根堆的堆顶的数要小,就把堆顶的数弹出,再插入当前遍历到的数。最后将大根堆里的数存入数组返回即可。

class Solution {
    public int[] smallestK(int[] arr, int k) {
        // 参数检测
        if(null == arr || k <= 0)
            return new int[0];
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        // 将数组中的元素依次放到堆中
        for(int i = 0; i < arr.length; ++i){
            q.offer(arr[i]);
        }
        // 将优先级队列的前k个元素放到数组中
        int[] ret = new int[k];
        for(int i = 0; i < k; ++i){
            ret[i] = q.poll();
        }
        return ret;
    }
}

  1. 时间复杂度:O(n* logk),其中 nn 是数组 arr 的长度。由于大根堆实时维护前 k 小值,所以插入删除都是 O(logk) 的时间复杂度,最坏情况下数组里 n 个数都会插入,所以一共需要 O(n*logk) 的时间复杂度。
  2. 空间复杂度:O(k),因为大根堆里最多 kk 个数。

在这里插入图片描述

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

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

相关文章

如何用两个晚上教女生学会Python

文章目录安装、需求引导和开发模型命令行计算器用温度指导穿衣VS Code 和女孩子的衣柜用遍历来挑选衣物交互课后作业事情的起因是这样的&#xff0c;知乎上有个妹纸加我&#xff0c;说要相亲。尽管我欣喜若狂&#xff0c;但恰巧在外出差&#xff0c;根本走不开。妹纸于是说要不…

自动化和半自动矢量化提取地物矢量轮廓

假期愉快&#xff08;这个假期加班了没&#xff1f;图片&#xff09;&#xff01;今天小助手来分享关于自动化和半自动化的矢量提取&#xff0c;使用的软件都是我们常用的软件。一是使用Global Mapper对遥感影像或矢量底图进行自动提取&#xff0c;二是基于天地图矢量底图使用A…

阶段性总结 | C语言

… &#x1f333;&#x1f332;&#x1f331;本文已收录至&#xff1a;技术之外的往事 更多知识尽在此专栏中&#xff01; &#x1f389;&#x1f389;&#x1f389;欢迎点赞、收藏、关注 &#x1f389;&#x1f389;&#x1f389;回顾过去 各位CSND的小伙伴们大家好&#xf…

C · 进阶 | 慎看!深剖文件操作,怕你停不下

啊我摔倒了..有没有人扶我起来学习.... 目录前言一、 什么是文件1.1 程序文件1.2 数据文件1.3 文件名二、文件的打开和关闭2.1 文件指针2.2 文件的打开和关闭三、文件的顺序读写3.0 有必要解释一下*3.1 fputc3.2 fgetc3.3 fprintf3.4 fscanf3.4.1来个小总结&#xff08;这里忽略…

双非本23秋招之路-从考研跑路到某安全大厂(无实习、项目)

文章目录双非本23秋招之路-从考研跑路到某安全大厂&#xff08;无实习、项目&#xff09;一、自我介绍二、简历准备三、刷题四、八股文五、项目方面六、关于实习七、面试方面八、秋招路程九、简历投递十、面经分享双非本23秋招之路-从考研跑路到某安全大厂&#xff08;无实习、…

springboot+jsp新闻发布投稿系统

本文采用JSP技术构建的一个管理系统&#xff0c;实现了一个新闻发布系统。新闻发布系统的主要实现功能包括&#xff1a;管理员&#xff1a;首页、个人中心、用户管理 、新闻分类管理 、新闻信息管理、新闻投稿管理、论坛管理、我的收藏管理、投诉建议管理、系统管理。前台首页&…

Python编程 print输出函数

作者简介&#xff1a;一名在校计算机学生、每天分享Python的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.输入与输出 1.print&#xff08;&#xff09;输出函数 2.sep 3.en…

【MySQL数据库和JDBC编程】第三章-第一节:MySQL的增删查改基础篇

文章目录一&#xff1a;INSET新增二&#xff1a;SELECT查询&#xff08;1&#xff09;全列查询&#xff08;2&#xff09;指定列查询&#xff08;3&#xff09;查询字段为表达式&#xff08;4&#xff09;起别名&#xff08;5&#xff09;去重&#xff08;DISTINCT&#xff09;…

微信小程序request:fail报错(包括不执行fail回调问题)

微信小程序request:fail报错&#xff08;包括不执行fail回调的问题&#xff09;1. 不执行fail回调的问题2. request:fail报错原因2.1 小程序未配置域名导致的错误2.2 微信小程序使用的服务器环境不支持TLS1.22.3 使用的SSL证书不信任2.4 SSL证书证书链缺乏2.5 域名未备案&#…

使用Spring框架进行Web项目开发(初级)

目录 前言 1. 为什么常规的Spring框架不适合Web项目呢&#xff1f; 2. 如何在Spring框架中创建容器&#xff1f; 3. Spring框架开发Web项目的步骤 3.1 创建maven项目 3.2 添加相应的依赖 3.3 在webapp目录下的web.xml中注册监听器 3.4 在webapp文件夹下的web.xml中配置…

【信息科学技术与创新】自然语言处理 NLP 计算机与智能 课程总结思考

深入了解 NLP 及课程总结反思 摘要 自然语言处理的历史发展自然语言处理的方法与相关应用关于数据智能科学技术导论这门课程的总结反思 Navigator深入了解 NLP 及课程总结反思一、自然语言处理的历史发展二、自然语言处理的方法与相关应用三、关于数据智能科学技术导论这门课…

【C++初阶】日期类实现、const成员函数、取地址及const取地址操作符重载

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【C学习与应用】 ✒️✒️本篇内容&#xff1a;日期类的代码实现、const成员函数的概念和作用、取地址及const取地址操作符重载 &#x1f6a2;&#x1f6a2…

去水印小程序

真正的大师,永远都怀着一颗学徒的心&#xff01; 一、项目简介 项目UI确实有点朴实无华&#xff0c;但并不影响她美丽的内在。这和人也一样&#xff0c;属于心灵美。 虽然&#xff0c;这个社会上的大多数人喜欢从一件事物的外表&#xff0c;去评判事物的好坏&#xff0c;即好…

vue支付项目-APP支付宝支付功能

⭐️⭐️⭐️ 作者&#xff1a;船长在船上 &#x1f6a9;&#x1f6a9;&#x1f6a9; 主页&#xff1a;来访地址船长在船上的博客 &#x1f528;&#x1f528;&#x1f528; 简介&#xff1a;CSDN前端领域优质创作者&#xff0c;资深前端开发工程师&#xff0c;专注前端开发…

FreeRTOS 软件定时器的使用

FreeRTOS中加入了软件定时器这个功能组件&#xff0c;是一个可选的、不属于freeRTOS内核的功能&#xff0c;由定时器服务任务&#xff08;其实就是一个定时器任务&#xff09;来提供。 软件定时器是当设定一个定时时间&#xff0c;当达到设定的时间之后就会执行指定的功能函数&…

【趣学算法】Day2 贪心算法——最优装载问题

14天阅读挑战赛努力是为了不平庸~ 算法学习有些时候是枯燥的&#xff0c;这一次&#xff0c;让我们先人一步&#xff0c;趣学算法&#xff01; ❤️一名热爱Java的大一学生&#xff0c;希望与各位大佬共同学习进步❤️ &#x1f9d1;个人主页&#xff1a;周小末天天开心 各位大…

ESP8266/esp32接入阿里云物联网平台点灯控制类案例

ESP8266/esp32接入阿里云物联网平台点灯控制类案例&#x1f4cc;阿里云物联网云平台介绍&#xff1a;https://help.aliyun.com/product/30520.html &#x1f38b;需要自己在阿里云物联网云平台注册自己的账户&#xff0c;这里不做介绍了。 &#x1f33b;阿里云物联网云平台创建…

Python基础入门(持续更新中)

一、发展历程 Python的创始人为荷兰人吉多范罗苏姆&#xff08;Guido van Rossum&#xff09;。1989年圣诞节期间&#xff0c;在阿姆斯特丹&#xff0c;Guido为了打发圣诞节的无趣&#xff0c;决心开发一个新的脚本解释程序&#xff0c;作为ABC语言的一种继承。之所以选中单词P…

CANoe-以太网软硬件网络自动映射的问题

以太网软硬件网络自动映射的问题 当我们设置CANoe以太网模式为Network-based access模式时 我们需要在CANoe硬件里配置segment,具体内容请参考文章《如何配置CANoe Network-based access模式的以太网网络拓扑》 有时我们图省事,配置的segment默认名称是什么,我们也不管,也…

UVM如何处理out-of-order乱序传输

文章目录前言1、基本思路2、支持乱序传输的sequence3、支持乱序传输的Driver总结前言 乱序传输(out-of-order)是指在协议中&#xff0c;后发出去的req&#xff0c;支持先回resp&#xff0c;通常通过ID来保证req和resp之间的关系。很多协议支持乱序传输&#xff0c;例如AXI4。本…