Java-数据结构-优先级队列(堆)

news2025/2/2 22:00:56

一、优先级队列

① 什么是优先级队列?

在此之前,我们已经学习过了"队列"的相关知识,我们知道"队列"是一种"先进先出"的数据结构,我们还学习过"栈"是"后进先出"的数据结构,这两种数据结构通常能够帮助我们在解题时存储一些数据,但是经过一段时间的练习,我们应该也能意识到这两种数据结构的缺点:"不够灵活"。

因为在有些情况下,我们要存储的数据并非是只存取队头或者队尾,有时我们还想使数据拥有一种排序的规律,使得我们能够随时取出和存入"按照这种规律下,优先级较高的数据",而想要实现这一点,即便是"栈"与"队列"两者的结合"双端队列",都是无法轻易办到的。

📚 比如:有时考试我们会按照学生的成绩进行分配考场,想在一个考场中提出或者放入一个学生,就要根据他的考试成绩来判断。

于是——优先级队列(PriorityQueue)出现了

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

优先级队列是一种能够按照指定的排序方式,自动将数据存到合理的位置的数据结构。它的底层实际上就是通过类似二叉树的结构来实现的~

优先级队列可以分为两种

📕 大根堆(这棵二叉树中,每棵树的根节点都比它左右子树的根节点大)

📕 小根堆(这棵二叉树中,每棵树的根节点都比它左右子树的根节点小)

(需要注意的是:根总是一棵完全二叉树)

📚 比如:此时我们有一组数据为{10,7,12,6,8,4},那么按照小根堆的形式存储它,就是:

这样的一颗二叉树,那么接下来我们一点点的去探究,优先级队列究竟是如何实现的:

① 基本框架

(java默认的优先级队列为小根堆,所以我们这里尝试模拟实现一个大根堆)

与之前一些数据结构的模拟实现较为类似,由于"堆"是一棵完全二叉树,所以这里我们的模拟实现就不再使用链表了。而是直接使用数组进行实现(通过层序遍历的顺序),所以我们需要最基本的一个整数数组elem,还需要一个记录有效数据个数的usedSize~

import java.util.*;



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


    public MyHeap() {

    }

    //对elem进行初始化
    public void init(int[] array) {
    
    }

    //创建一个优先级队列
    public void createHeap(int[] array) {
        
    }

    //向下调整
    private void shiftDown(int root,int len) {
        
    }

    //元素入队(不破坏优先级队列结构)
    public void push(int val) {
        
    }

    //向上调整
    private void shiftUp(int child) {
        
    }

    //判满
    public boolean isFull() {
        
    }

    //出队
    public void pollHeap() {
        
    }

    //判空
    public boolean isEmpty() {
        
    }

    //获取队顶元素
    public int peekHeap() {
        
    }
}

② 初始化elem

📚 思路提示

该方法用于我们先将数组存入elem中,以便我们后续的操作,我们只需要将数组中的元素存入elem,并且在初始化elem途中,适时的对elem进行扩容即可~

📖 代码示例

    //对elem进行初始化
    public void init(int[] array) {
        int len = array.length;
        for (int i = 0; i < len; i++) {
            if (isFull()) {
                elem = Arrays.copyOf(elem, elem.length * 2);
            }
            elem[i] = array[i];
            usedSize++;
        }
    }
    //判满
    public boolean isFull() {
        return usedSize == elem.length;
    }

③ 堆的向下调整

📚 思路提示

想要创建一个大根堆,就需要我们上面提到的大根堆性质"每棵树的根节点都比它左右子树的根节点大"

而想要使我们的elem中的元素也遵从这种规律,我们就要知道应该如何去对堆中的元素位置进行调整。其实还是比较简单的:

📚 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有结点从0开始编号,则对于序号为i的结点有以下性质:

📕 若孩子节点下标为 i ,那么父亲结点为 (i - 1) / 2 [i > 0]

📕 若父亲节点下标为 i ,那么左孩子节点为 2 * i + 1 [2i + 1 < n],右孩子节点为 2 * i + 2  [2i + 2 < n]

📕 首先,将需要调整的元素下标记作parent

📕 如果parent存在左孩子,则通过公式,算出parent的左孩子结点child

📕 如果有两个孩子结点,则将孩子结点标记为值最大的孩子

📕 判断孩子结点与父亲结点的大小,如果孩子更大,则交换孩子与父亲结点(大根堆)

📕 如果交换了结点,则将parent标记为child,再重新求child

图示

📖 代码示例

    //向下调整
    private void shiftDown(int parent,int len) {
        int child = parent * 2 + 1;
        while(child < len){
            //判断是否有右孩子,并且右孩子是否更大
            if(child + 1 < len && elem[child + 1] > elem[child]){
                child++;
            }
            if(elem[child] > elem[parent]){
                swap(elem,child,parent);
                parent = child;
                child = parent * 2 + 1;
            }else {
                break;
            }
        }
    }
    public void swap(int[] arr,int i,int j){
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

④ 堆的创建

📚 思路提示

我们刚刚所实现的"向下调整",其实就是为了这一步我们成功的构建出一个"大根堆"而进行的铺垫

我们可以注意到,在上面的"向上调整"中,我们每一次进行交换,其实就是创建出了一个"这三个元素组成的局部大根堆",而想将整个数组都变成大根堆,我们就可以对数组中每一个有子节点的根进行向下调整~

📖 代码示例

    //创建一个优先级队列
    public void createHeap() {
        int len = elem.length;
        int parent = (elem.length - 1 - 1) / 2;
        for(int i = parent;i >= 0;i--){
            shiftDown(i,len);
        }
    }

这里或许大家会有一个疑问:为什么从最后一个根节点向上调整,而不是从第一个根节点向下调整?这是因为,两种看似一样的方法,其实时间复杂度相差并不小:

📕 如果从第一个根节点开始向下调整

对于根节点(位于第 1 层),它可能需要向下调整到叶子节点(第 log n 层),因此调整的时间复杂度是 O(log n)。

对于第 2 层的节点,它们可能需要向下调整到第 log n - 1 层,时间复杂度是 O(log n - 1)。

以此类推,直到叶子节点(不需要调整)。

这种方式的调整,时间复杂度为O(nlogn)

📕 如果从最后一个根节点向上调整

直接能够越过所有的叶子节点(最好的情况占结点总数一半 + 1)

对于剩余的结点,调整取决于它们的高度,并且越接近根节点,需要调整的高度越低。

这种方式的调整,时间复杂度接近O(n)

⑤ 堆的插入

📚 思路提示

顾名思义,就是在堆中插入一个元素,但是我们需要注意的是,不论是此刻的插入,亦或是之后我们需要进行的删除,都要保证操作后仍然为大根堆。所以在我们将新的元素插入到堆的末尾后,还要将该元素不断地向上进行调整,直到在某一刻符合大根堆的条件,停止调整。

📕 对堆进行判满,如果堆已满则进行扩容

📕 将新的元素插入堆的末尾,usedSize++

📕 对新元素进行向上调整:

📕 将元素下标标记为child,并且通过公式求出它的parent

📕 如果 parent >= 0 判断child和parent的值,如果child更大,两者进行交换否则调整结束

(因为在此之前,已经是标准的大根堆,所以不需要进行两个child的判断,只调整目标结点即可)

📕 如果进行了交换,则再将parent = child,然后重新求parent

图示

📖 代码示例

    //元素入队(不破坏优先级队列结构)
    public void push(int val) {
        if(isFull()){
            elem = Arrays.copyOf(elem, elem.length * 2);
        }
        elem[usedSize] = val;
        shiftUp(usedSize++);
    }
    //向上调整
    private void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while(parent >= 0){
            if(elem[child] > elem[parent]){
                swap(elem,child,parent);
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }

⑥ 堆的删除

(注意:堆的删除指的是删除堆顶元素)

📚 思路提示

想要删除一个堆顶元素,我们首先知道的是"usedSize"需要减一,那么我们由这点思考一下,如果不进行任何操作,首先usedSize--后,我们的堆中实际上消失的是哪个元素?没错,就是堆中的最后一个元素~,但是我们想删除的并不是该元素,而是我们的堆顶元素,简单呀~将这两个元素互换一下不就好了~最后再将新的堆顶元素进行向下调整即可~

📕 首先,判断该堆是否为空,若为空则直接退出该方法

📕 将堆顶元素与堆中最后一个元素进行交换

📕 然后将usedSize--

📕 最后将新的堆顶元素进行向下调整

图示

📖 代码示例

    //出队
    public void pollHeap() {
        if(isEmpty()){
            return;
        }
        swap(elem,0,--usedSize);
        shiftDown(0,usedSize);
    }
    //判空
    public boolean isEmpty() {
        return usedSize == 0;
    }
    //获取队顶元素
    public int peekHeap() {
        return elem[0];
    }

完整代码:

import java.util.*;

public class MyHeap {
    public int[] elem;
    public int usedSize;
    public MyHeap(){
        elem = new int[10];
    }
    //创建一个大根堆
    public void creative(int[] arr){
        for(int i = 0;i < arr.length;i++){
            if(isFull()){
                elem = Arrays.copyOf(elem,elem.length * 2);
            }
            elem[i] = arr[i];
            usedSize++;
        }
    }
    public void doArr(){
        //最后一个叶子结点的父结点
        int start = (usedSize - 1 - 1) / 2;
        for(int i = start;i >= 0;i--){
            siftDown(i,usedSize);
        }
    }
    //将较大的子节点与父结点交换
    //向下调整
    public void siftDown(int parent,int end){
        //找到父结点的的左子结点
        int child = parent * 2 + 1;
        //保证该子结点未越界
        while(child < end){
            //查找左右子结点中的最大子结点
            //(包含了找出最大值后,对右子节点的判断)
            if(child + 1 < end && 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 = parent * 2 + 1;
            }else {
                break;
            }
        }
    }
    //向堆中添加新元素
    public void offer(int val){
        if(isFull()){
            elem = Arrays.copyOf(elem,elem.length * 2);
        }
        elem[usedSize] = val;
        siftUp(usedSize);
        usedSize++;
    }
    //向上调整
    public void siftUp(int child){
        int parent = (child - 1) / 2;
        while(parent >= 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 int poll(){
        int oldValue = elem[0];
        elem[0] = elem[--usedSize];
        siftDown(0,usedSize);
        return oldValue;

    }
    //将堆排序成从小到大的顺序
    public void heapSort(){
        int end = usedSize - 1;
        while(end > 0){
            int tmp = elem[end];
            elem[end] = elem[0];
            elem[0] = tmp;
            siftDown(0,end);
            end--;
        }
    }
    public boolean isFull(){
        return usedSize >= elem.length;
    }
}

这篇文章我们对"优先级队列"进行了基本的认识,并且自己也尝试着模拟实现了一个大根堆,关于"优先级队列"的后续知识我将在后面的文章中为大家讲解,那么这篇文章到这里就结束啦,作者能力有限,如果有哪里说的不够清楚或者不够准确,还请各位在评论区多多指出,我也会虚心学习的,我们下次再见啦

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

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

相关文章

爬虫基础(四)线程 和 进程 及相关知识点

目录 一、线程和进程 &#xff08;1&#xff09;进程 &#xff08;2&#xff09;线程 &#xff08;3&#xff09;区别 二、串行、并发、并行 &#xff08;1&#xff09;串行 &#xff08;2&#xff09;并行 &#xff08;3&#xff09;并发 三、爬虫中的线程和进程 &am…

C语言初阶力扣刷题——349. 两个数组的交集【难度:简单】

1. 题目描述 力扣在线OJ题目 给定两个数组&#xff0c;编写一个函数来计算它们的交集。 示例&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 输入&#xff1a;nums1 [4,9,5], nums2 [9,4,9,8,4] 输出&#xff1a;[9,4] 2. 思路 直接暴力…

Sqoop导入MySQL中含有回车换行符的数据

个人博客地址&#xff1a;Sqoop导入MySQL中含有回车换行符的数据 MySQL中的数据如下图&#xff1a; 检查HDFS上的目标文件内容可以看出&#xff0c;回车换行符位置的数据被截断了&#xff0c;导致数据列错位。 Sqoop提供了配置参数&#xff0c;在导入时丢弃掉数据的分隔符&…

LightM-UNet(2024 CVPR)

论文标题LightM-UNet: Mamba Assists in Lightweight UNet for Medical Image Segmentation论文作者Weibin Liao, Yinghao Zhu, Xinyuan Wang, Chengwei Pan, Yasha Wang and Liantao Ma发表日期2024年01月01日GB引用> Weibin Liao, Yinghao Zhu, Xinyuan Wang, et al. Ligh…

stm32硬件实现与w25qxx通信

使用的型号为stm32f103c8t6与w25q64。 STM32CubeMX配置与引脚衔接 根据stm32f103c8t6引脚手册&#xff0c;采用B12-B15四个引脚与W25Q64连接&#xff0c;实现SPI通信。 W25Q64SCK&#xff08;CLK&#xff09;PB13MOSI&#xff08;DI&#xff09;PB15MISO(DO)PB14CS&#xff08…

FPGA 使用 CLOCK_DEDICATED_ROUTE 约束

使用 CLOCK_DEDICATED_ROUTE 约束 CLOCK_DEDICATED_ROUTE 约束通常在从一个时钟区域中的时钟缓存驱动到另一个时钟区域中的 MMCM 或 PLL 时使 用。默认情况下&#xff0c; CLOCK_DEDICATED_ROUTE 约束设置为 TRUE &#xff0c;并且缓存 /MMCM 或 PLL 对必须布局在相同…

一个开源 GenBI AI 本地代理(确保本地数据安全),使数据驱动型团队能够与其数据进行互动,生成文本到 SQL、图表、电子表格、报告和 BI

一、GenBI AI 代理介绍&#xff08;文末提供下载&#xff09; github地址&#xff1a;https://github.com/Canner/WrenAI 本文信息图片均来源于github作者主页 在 Wren AI&#xff0c;我们的使命是通过生成式商业智能 &#xff08;GenBI&#xff09; 使组织能够无缝访问数据&…

C动态库的生成与在Python和QT中的调用方法

目录 一、动态库生成 1&#xff09;C语言生成动态库 2&#xff09;c类生成动态库 二、动态库调用 1&#xff09;Python调用DLL 2&#xff09;QT调用DLL 三、存在的一些问题 1&#xff09;python调用封装了类的DLL可能调用不成功 2&#xff09;DLL格式不匹配的问题 四、…

C++ Primer 自定义数据结构

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

解析 Oracle 中的 ALL_SYNONYMS 和 ALL_VIEWS 视图:查找同义词与视图的基础操作

目录 前言1. ALL_SYNONYMS 视图2. ALL_VIEWS 视图3. 扩展 前言 &#x1f91f; 找工作&#xff0c;来万码优才&#xff1a;&#x1f449; #小程序://万码优才/r6rqmzDaXpYkJZF 1. ALL_SYNONYMS 视图 在 Oracle 数据库中&#xff0c;同义词&#xff08;Synonym&#xff09;是对数…

PyTorch框架——基于深度学习YOLOv8神经网络学生课堂行为检测识别系统

基于YOLOv8深度学习的学生课堂行为检测识别系统&#xff0c;其能识别三种学生课堂行为&#xff1a;names: [举手, 读书, 写字] 具体图片见如下&#xff1a; 第一步&#xff1a;YOLOv8介绍 YOLOv8 是 ultralytics 公司在 2023 年 1月 10 号开源的 YOLOv5 的下一个重大更新版本…

7.攻防世界fileclude

题目描述 进入题目页面如下 看到题目提示应该为文件包含漏洞 解释上述代码 // 输出提示信息&#xff1a;错误的方式&#xff01; WRONG WAY! <?php // 包含名为 "flag.php" 的文件&#xff0c;通常这个文件里可能包含重要的敏感信息&#xff0c;如 flag inclu…

【自然语言处理(NLP)】深度学习架构:Transformer 原理及代码实现

文章目录 介绍Transformer核心组件架构图编码器&#xff08;Encoder&#xff09;解码器&#xff08;Decoder&#xff09; 优点应用代码实现导包基于位置的前馈网络残差连接后进行层规范化编码器 Block编码器解码器 Block解码器训练预测 个人主页&#xff1a;道友老李 欢迎加入社…

【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(二)

目录 1 -> HML语法 1.1 -> 页面结构 1.2 -> 数据绑定 1.3 -> 普通事件绑定 1.4 -> 冒泡事件绑定5 1.5 -> 捕获事件绑定5 1.6 -> 列表渲染 1.7 -> 条件渲染 1.8 -> 逻辑控制块 1.9 -> 模板引用 2 -> CSS语法 2.1 -> 尺寸单位 …

当WebGIS遇到智慧文旅-以长沙市不绕路旅游攻略为例

目录 前言 一、旅游数据组织 1、旅游景点信息 2、路线时间推荐 二、WebGIS可视化实现 1、态势标绘实现 2、相关位置展示 三、成果展示 1、第一天旅游路线 2、第二天旅游路线 3、第三天旅游路线 4、交通、订票、住宿指南 四、总结 前言 随着信息技术的飞速发展&…

使用Pygame制作“吃豆人”游戏

本篇博客展示如何使用 Python Pygame 编写一个简易版的“吃豆人&#xff08;Pac-Man&#xff09;” 风格游戏。这里我们暂且命名为 Py-Man。玩家需要控制主角在一个网格地图里移动、吃掉散布在各处的豆子&#xff0c;并躲避在地图中巡逻的幽灵。此示例可帮助你理解网格地图、角…

SQL入门到精通 理论+实战 -- 在 MySQL 中学习SQL语言

目录 一、环境准备 1、MySQL 8.0 和 Navicat 下载安装 2、准备好的表和数据文件&#xff1a; 二、SQL语言简述 1、数据库基础概念 2、什么是SQL 3、SQL的分类 4、SQL通用语法 三、DDL&#xff08;Data Definition Language&#xff09;&#xff1a;数据定义语言 1、操…

x86-64数据传输指令

关于汇编语言一些基础概念的更详细的介绍&#xff0c;可移步MIPS指令集&#xff08;一&#xff09;基本操作_mips指令 sw-CSDN博客 该指令集中一个字2字节。 该架构有16个64位寄存器&#xff0c;名字都以%r开头&#xff0c;每个寄存器的最低位字节&#xff0c;低1~2位字节&…

【ESP32】ESP-IDF开发 | WiFi开发 | TCP传输控制协议 + TCP服务器和客户端例程

1. 简介 TCP&#xff08;Transmission Control Protocol&#xff09;&#xff0c;全称传输控制协议。它的特点有以下几点&#xff1a;面向连接&#xff0c;每一个TCP连接只能是点对点的&#xff08;一对一&#xff09;&#xff1b;提供可靠交付服务&#xff1b;提供全双工通信&…

算法基础学习——快排与归并(附带java模版)

快速排序和归并排序是两种速度较快的排序方式&#xff0c;是最应该掌握的两种排序算法&#xff0c; &#xff08;一&#xff09;快速排序&#xff08;不稳定的&#xff09; 基本思想&#xff1a;分治 平均时间复杂度&#xff1a;O(nlogn) / 最慢O(n^2) / 最快O(n) 步骤&…