数据结构 之 优先级队列(堆) (PriorityQueue)

news2024/11/18 20:48:25

🎉欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨

🎉感谢各位读者在百忙之中抽出时间来垂阅我的文章,我会尽我所能向的大家分享我的知识和经验📖

🎉希望我们在一篇篇的文章中能够共同进步!!!

🌈个人主页:AUGENSTERN_dc

🔥个人专栏:C语言 | Java | 数据结构

⭐个人格言:

一重山有一重山的错落,我有我的平仄

一笔锋有一笔锋的着墨,我有我的舍得

目录

1.概念:

2. 堆的分类:

2.1 大根堆:

2.2 小根堆:

3. 堆的创建:

3.1 堆类的构建:

3.2 双亲节点和孩子节点的关系:

3.3 堆的初创建:

3.4 向下调整:

3.5 向上调整:

4. 优先级队列的模拟实现:

5. 优先级队列的模拟实现整体源码:


1.概念:

在我们之前的队列的文章中介绍过,队列是一种先进先出的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。

如果我们给每个元素都分配一个数字来标记其优先级,不妨设较小的数字具有较高的优先级,(在后续中我们会讲到,这是小根堆)这样我们就可以在一个集合中访问优先级最高的元素并对其进行查找和删除操作了。这样,我们就引入了优先级队列这种数据结构。 优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有

(1)查找

(2)插入一个新元素

(3)删除

一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。

在jdk1.8中,PriorityQueue的底层是用堆这一数据结构实现的;

2. 堆的分类:

堆在逻辑上是一颗完全二叉树,但是堆的实现却是由数组实现的,我们是将这颗完全二叉树按照层序遍历的方式存放在数组中的;

堆分为两种:

2.1 大根堆:

大根堆是指根节点的值最大左右子节点的值都小于根节点的完全二叉树按照层序遍历的方式存放到数组中的一个堆结构;

要想真正的了解堆这个结构,我们不妨从绘图开始理解:

首先我们试着画一个完全二叉树:

将上图的完全二叉树按照层序遍历的方式存放在数组中,如上图,就是一个大根堆;

我们会发现,在上图中的完全二叉树中,根节点25 的值是最大的,根节点25的左右节点的值都比25要小,同时,我们会发现 ,20节点和17节点的左右节点的值同样小于根节点的值;

这就是大根堆的特性;

2.2 小根堆:

小根堆和大根堆则相反,根节点的值要小于左右节点的值;

如下图

3. 堆的创建:

(!!!在接下来的内容中,所有的堆都以小根堆的形式创建!!!)

3.1 堆类的构建:

public class my_PriorityQueue {
    public int[] elem;              //堆的底层是数组实现的;
    public int usedSize;            //数组的使用长度

    private static final int DEFAULT_INITIAL_CAPACITY = 11;     //数组的默认长度

    public my_PriorityQueue() {
        //不带参数的构造方法
    }
}

3.2 双亲节点和孩子节点的关系:

如果堆的存储结构也是一颗完全二叉树的话,想要从根节点找到左右子树是很简单的事情,但是堆的存储结构是一个数组,那么我们又要如何才能找到他的左右子树呢?

以上图的小根堆为例:

由于堆的底层是由数组实现的,那么每一个节点都会有一个对应的下标,我们将其标明在图上;

下标为0的节点的左右子树为1  和  2;下标为1的节点的左右子树的节点的下标为3 和 4;

假设双亲节点为  parent, 孩子节点为 child, 我们不难发现parent 和 child的关系:

(child - 1)/ 2 = parent

这就是双亲节点和孩子节点的关系;

有了这个关系,我们就可以开始试着创建堆了;

3.3 堆的初创建:

假如我们有一个空的小根堆,我们开始向空堆中插入元素,我们先插入值为4 的元素;

接下来为了保持小根堆这个结构,在插入元素之后,我们就需要开始考虑了;

首先我们将元素直接插在4的后面;

如果我们插入的值比插入节点的双亲节点(也就是4节点)大,我们应该保持插入元素的位置不变,

但是如果我们插入的元素比4小呢?  我们就应该将该节点和4节点交换位置;

如图:

那是不是,每次我们插入元素的时候,我们需要进行比较和判断;

看插入的元素的大小和其双亲节点的大小相较之下如何;

但是,随着元素的增多:

如果我们插入一个值为2的节点,我们发现,我们不仅需要2和15进行狡猾,并且交换玩之后,我们需要将2和5再次进行交换,这就会影响整棵完全二叉树;

同时我们会发现,我们有两种调整的方法,我们称为向下调整向上调整

在创建堆的时候我们一般使用向下调整:

我们用createHeap表示创建小根堆方法,  shiftDown表示向下调整方法;

public void createHeap() {
        elem = new int[DEFAULT_INITIAL_CAPACITY];       //构造一个空的,大小为默认大小的堆
    }

    public void createHeap(int[] array) {               //构造一个元素和array数组相同的array小根堆
        elem = array.clone();
        usedSize = array.length;
        int child = array.length - 1;                   //孩子节点的下标
        int parent = (child - 1) / 2;                   //双亲节点的下标
        while (parent >= 0) {                           //从最后一个孩子节点的双亲节点一直向下调整到下标为0的节点
            shiftDown(parent, usedSize - 1);        //向下调整
            parent--;                           
        }
    }

3.4 向下调整:

以上图为例:向下调整就是从最后一个元素的双亲节点开始,依次和子节点比较大小,若需要互换,则进行调整,直到双亲节点的下标为0为止;

如图,就是依次将值为5的节点和值为22的节点, 值为15的节点中的最大值比较,若需要交换则进行调整,一直从值为5的节点调整到值为2的节点为止;

向下调整一般在创建堆的时候进行使用:

接下来我们开始对小根堆的创建:

首先我们先随意给定一个整形数组,将其按照完全二叉树的逻辑排列

最后一个节点下标为5;那么其双亲节点为 ( 5 - 1)/ 2 = 2;

随后我们需要判断下标为2的节点和下标为5的节点的大小,一直从下标为2的节点判断到下标为0的节点为止;

代码的思路大概构建出来了,我们开始着手写向下调整的代码:

private void swap(int x, int y) {
        int tmp = elem[x];
        elem[x] = elem[y];
        elem[y] = tmp;
    }
private void shiftDown(int root,int len) {
        int parent = root;
        int child = parent * 2 + 1;
        while (child <= len) {                                              //若有两个孩子节点
            child = elem[child] < elem[child + 1] ? child : child + 1;      //找出两个孩子节点中的最大的节点
            if (elem[parent] > elem[child]) {                               //若孩子节点大于双亲节点
                swap(child, parent);                                        //交换孩子节点和双亲节点
                parent = child;                                             //将parent重置为child
                child = parent * 2;                                         //重置child,判断交换后的子树是否需要调整
            } else {
                break;                                                      //若无需调整,则直接跳出循环
            }
        }
    }

3.5 向上调整:

向上调整一般在插入元素的时候使用,例如在已经创建完成的堆中插入一个元素,一般是先将该元素放在数组的最后,然后依次将其和双亲节点进行大小比较,直到孩子节点的下标为0或者不需要和双亲节点进行交换为止,如图所示:

在这样一个小根堆中,我们插入一个元素3

此时的child = 7,parent = 3,首先我们来判断3和17 的大小,很明显,需要交换:

交换完成之后,我们将child = parent = 3,此时的parent = 1;

此时我们继续判断child和parent 的大小关系,还是需要交换3 和 6,再将child = parent,

parent = (child + 1)* 2 = 0;

继续比较child和parent的值的大小关系,发现并不需要比较了,那我们就停止判断即可;

这就是向上调整的思路:

private void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (parent >= 0 && child != 0) {
            if (elem[child] < elem[parent]) {       
                swap(child, parent);
                child = parent;
                parent = (child - 1) / 2;
            } else  {
                break;
            }
        }
    }

4. 优先级队列的模拟实现:

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

和队列的模拟实现类似,优先级队列同样有插入元素删除元素获得队头元素的方法:

< 1 >  插入元素:

每次插入元素之前,我们需要判断堆是否满了,若满了,则进行扩容:

private void grow() {
        int len = elem.length;
        if (len < 64) {
            elem = Arrays.copyOf(elem, DEFAULT_INITIAL_CAPACITY * 2);
        } else {
            elem = Arrays.copyOf(elem, DEFAULT_INITIAL_CAPACITY + DEFAULT_INITIAL_CAPACITY >> 1);
        }
    }

判断完成后,我们需要将插入元素后的新堆调整为大根堆或者小根堆,我们这里以小根堆为例:

public void offer(int val) {
        if (isFull()) {
            grow();
        }
        if (usedSize == 0) {
            elem[0] = val;
            usedSize++;
            return;
        }
        elem[usedSize] = val;
        shiftUp(usedSize);
        usedSize++;
    }

< 2 >  删除元素:

由于删除元素是将堆顶元素进行删除,我们可以先将堆顶元素和堆末尾的元素进行交换,将堆末尾的元素删除也就是将usedsize - - 即可;

public void pollHeap() {
        swap(0, usedSize - 1);
        usedSize--;
        shiftDown(0, usedSize - 1);
    }

< 3 >  获得队头元素:

public int peekHeap() {
        return elem[0];
    }

还有size()  , isEmpty() ,clear()方法,由于太简单,这里就没有写了;

5. 优先级队列的模拟实现整体源码:

import java.util.Arrays;

public class my_PriorityQueue {
    public int[] elem;              //堆的底层是数组实现的;
    public int usedSize;            //数组的使用长度

    private static final int DEFAULT_INITIAL_CAPACITY = 11;     //数组的默认长度

    public my_PriorityQueue() {
        //不带参数的构造方法
    }

    /**
     * 建堆的时间复杂度:
     *
     * @param array
     */

    public void createHeap() {
        elem = new int[DEFAULT_INITIAL_CAPACITY];       //构造一个空的,大小为默认大小的堆
    }

    public void createHeap(int[] array) {               //构造一个元素和array数组相同的array小根堆
        elem = array.clone();
        usedSize = array.length;
        int child = array.length - 1;                   //孩子节点的下标
        int parent = (child - 1) / 2;                   //双亲节点的下标
        while (parent >= 0) {                           //从最后一个孩子节点的双亲节点一直向下调整到下标为0的节点
            shiftDown(parent, usedSize - 1);        //向下调整
            parent--;
        }
    }

    /**
     *
     * @param root 是每棵子树的根节点的下标
     * @param len  是每棵子树调整结束的结束条件
     * 向下调整的时间复杂度:O(logn)
     */
    private void shiftDown(int root,int len) {
        int parent = root;
        int child = parent * 2 + 1;
        while (child <= len) {                                              //若有两个孩子节点
            child = elem[child] < elem[child + 1] ? child : child + 1;      //找出两个孩子节点中的最大的节点
            if (elem[parent] > elem[child]) {                               //若孩子节点大于双亲节点
                swap(child, parent);                                        //交换孩子节点和双亲节点
                parent = child;                                             //将parent重置为child
                child = parent * 2;                                         //重置child,判断交换后的子树是否需要调整
            } else {
                break;                                                      //若无需调整,则直接跳出循环
            }
        }
    }


    /**
     * 入队:仍然要保持是小根堆
     * @param val
     */
    public void offer(int val) {
        if (isFull()) {
            grow();
        }
        if (usedSize == 0) {
            elem[0] = val;
            usedSize++;
            return;
        }
        elem[usedSize] = val;
        shiftUp(usedSize);
        usedSize++;
    }

    private void shiftUp(int child) {
        int parent = (child - 1) / 2;
        while (parent >= 0 && child != 0) {
            if (elem[child] < elem[parent]) {
                swap(child, parent);
                child = parent;
                parent = (child - 1) / 2;
            } else  {
                break;
            }
        }
    }

    private void swap(int x, int y) {
        int tmp = elem[x];
        elem[x] = elem[y];
        elem[y] = tmp;
    }

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

    private void grow() {
        int len = elem.length;
        if (len < 64) {
            elem = Arrays.copyOf(elem, DEFAULT_INITIAL_CAPACITY * 2);
        } else {
            elem = Arrays.copyOf(elem, DEFAULT_INITIAL_CAPACITY + DEFAULT_INITIAL_CAPACITY >> 1);
        }
    }

    /**
     * 出队【删除】:每次删除的都是优先级高的元素
     * 仍然要保持是大根堆
     */
    public void pollHeap() {
        swap(0, usedSize - 1);
        usedSize--;
        shiftDown(0, usedSize - 1);
    }

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

    /**
     * 获取堆顶元素
     * @return
     */
    public int peekHeap() {
        return elem[0];
    }
}

以上就是优先级队列的全部内容了,感谢大家的收看,谢谢!!!!

如果觉得文章不错的话,麻烦大家三连支持以下ಠ_ಠ

制作不易,三连支持

谢谢!!!

以上的模拟实现代码未必是最优解,仅代表本人的思路,望多多理解,谢谢!!

最后送给大家一句话,同时也是对我自己的勉励:

生而有翼,怎么甘心匍匐一生,形如蝼蚁?

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

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

相关文章

langchain学习(十二)

Chat Messages | &#x1f99c;️&#x1f517; Langchain ChatMessageHistory&#xff1a;基类&#xff0c;保存HumanMessages、AIMessages from langchain.memory import ChatMessageHistory history ChatMessageHistory() history.add_user_message("hi!") his…

使用docker-compose管理freeswitch容器

概述 之前的文章我们介绍过如何将freeswitch做成docker镜像&#xff0c;也使用命令行模式正常启动了fs的docker容器。 但是当我们需要同时管理多个docker容器的时候&#xff0c;还是使用docker-compose更简单。 环境 CENTOS 7 docker engine&#xff1a;Version 25.0.3 D…

【深度学习与神经网络】MNIST手写数字识别1

简单的全连接层 导入相应库 import torch import numpy as np from torch import nn,optim from torch.autograd import Variable import matplotlib.pyplot as plt from torchvision import datasets, transforms from torch.utils.data import DataLoader读入数据并转为ten…

IDEA中在Service中开启管理多个微服务

问题 : 现在的service窗口里面什么都没有 ; 解决 : 1.没有service情况 : 点击View->Tool Windows -> Services,打开Service 2 . 在Service栏里操作 : 点击Add service &#xff0c; 然后选择第一个 : 然后在出来的选项中找到自己的项目类型 &#xff0c; 点击一下…

贪心算法(算法竞赛、蓝桥杯)--糖果传递

1、B站视频链接&#xff1a;A31 贪心算法 P2512 [HAOI2008] 糖果传递_哔哩哔哩_bilibili 题目链接&#xff1a;[HAOI2008] 糖果传递 - 洛谷 #include <bits/stdc.h> using namespace std; const int N1000005; int n,a[N],c[N]; long long b,ans;int main(){scanf(&quo…

Docker Compose基本配置及使用笔记

Docker Compose基本配置及使用笔记 简介 Docker Compose 是一个用于定义和运行多个 Docker 容器应用程序的工具。它使用 YAML 文件来配置应用程序的服务&#xff0c;并通过简单的命令集管理这些服务的生命周期。 1.步骤1 代码如下&#xff1a;docker-compose.yml放在虚拟机roo…

Mac版Jmeter安装与使用模拟分布式环境

Mac版Jmeter安装与使用&模拟分布式环境 1 安装Jmeter 1.1 安装Java环境 国内镜像地址&#xff1a;https://repo.huaweicloud.com/java/jdk/11.0.29/jdk-11.0.2_osx-x64_bin.dmg 下载dmg后&#xff0c;双击进行安装。 配置环境变量&#xff1a; # 1 打开环境变量配置文件…

js实现扫描线填色算法使用canvas展示

算法原理 扫描线填色算法的基本思想是&#xff1a;用水平扫描线从上到下扫描由点线段构成的多段构成的多边形。每根扫描线与多边形各边产生一系列交点。将这些交点按照x坐标进行分类&#xff0c;将分类后的交点成对取出&#xff0c;作为两个端点&#xff0c;以所填的色彩画水平…

结构体联合体枚举和位段

文章目录 结构体结构体类型的声明特殊的声明 结构的自引用结构体变量的定义和初始化结构体内存对齐为什么要内存对齐结构体传参结构体实现位段&#xff08;位段的填充&可移植性&#xff09;位段位段的内存分配空间如何开辟位段的跨平台问题位段的应用 枚举枚举类型的定义枚…

网络学习:IPV6报文详解

目录 前言&#xff1a; IPV6报文格式 IPV6基本报头 IPV6扩展报头 前言&#xff1a; 首先IPV6是属于网络层的一种协议&#xff0c;作为下一代IP协议&#xff0c;而想要学习一种协议就必不可少的需要去具体的研究协议报文中的各个参数以及其对应的功能作用。 IPV6报文格式 I…

在命令行中输入py有效,输入python无效,输入python会跳转到microsoft store

这里写自定义目录标题 如果你已经尝试过将python添加到系统变量如果你还未将python添加到系统变量没有python安装包且没有配置系统变量 如果你已经尝试过将python添加到系统变量 打开 运行&#xff0c;输入cmd&#xff0c;在命令行中输入 where python。 如果看到了这个 win…

Java基础-集合_上

文章目录 1.基本介绍2.集合的框架体系&#xff08;单列、双列&#xff09;单列集合双列集合比较 3.Collection接口和常用方法1.Collection接口实现类的特点2.常用方法&#xff08;使用ArrayList演示&#xff09;代码结果 3.迭代器遍历基本介绍代码结果 4.增强for循环遍历代码结…

滑动窗口和螺旋矩阵

209. 长度最小的子数组 题目 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 连续 子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度**。**如果不存在符合条件的子数组&#xff0c;返回…

十八、多线程JUC

目录 一、什么是多线程二、多线程的两个概念&#xff08;并发和并行&#xff09;三、多线程的实现方式3.1 继承Thread类的方式进行实现3.2 实现Runnable接口的方式进行实现3.3 利用Callable接口和Future接口方式实现 四、常见的成员方法五、线程的生命周期六、线程安全的问题七…

Nacos安装与集群搭建

Nacos安装与集群搭建 Nacos安装指南1.Windows安装1.1.下载安装包1.2.解压1.3.端口配置1.4.启动1.5.访问 2.Linux安装2.1.安装JDK2.2.上传安装包2.3.解压2.4.端口配置2.5.启动 3.Nacos的依赖Nacos集群搭建1.集群结构图2.搭建集群2.1.初始化数据库2.2.配置Nacos2.3.启动2.4.nginx…

JavaScript进阶:js的一些学习笔记-this指向,call,apply,bind,防抖,节流

文章目录 1. this指向1. 箭头函数 this的指向 2. 改变this的指向1. call()2. apply()3. bind() 3. 防抖和节流1. 防抖2. 节流 1. this指向 1. 箭头函数 this的指向 箭头函数默认帮我们绑定外层this的值&#xff0c;所以在箭头函数中this的值和外层的this是一样的箭头函数中的…

Linux——线程池

线程池的概念 线程池也是一种池化技术&#xff0c;可以预先申请一批线程&#xff0c;当我们后续有任务的时候就可以直接用&#xff0c;这本质上是一种空间换时间的策略。 如果有任务来的时候再创建线程&#xff0c;那成本又要提高&#xff0c;又要初始化&#xff0c;又要创建数…

揭示数据在内存中存储的秘密!

** ** 悟已往之不谏&#xff0c;知来者犹可追 ** ** 创作不易&#xff0c;宝子们&#xff01;如果这篇文章对你们有帮助的话&#xff0c;别忘了给个免费的赞哟~ 整数在内存中的存储 整数的表达方式有三种&#xff1a;原码、反码、补码。 三种表示方法均有符号位和数值位两部分…

【Java】List, Set, Queue, Map 区别?

目录 List, Set, Queue, Map 区别&#xff1f; Collection和Collections List ArrayList 和 Array区别&#xff1f; ArrayList与LinkedList区别? ArrayList 能添加null吗&#xff1f; ArrayList 插入和删除时间复杂度&#xff1f; LinkedList 插入和删除时间复杂度&…

51单片机基础篇系列-定时/计数器的控制工作方式

&#x1f308;个人主页&#xff1a;会编程的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” 定时/计数器的控制 80C51单片机定时/计数器的工作由两个特殊功能寄存器控制&#xff0c;TMOD用于设置其工作方式&#xff1a; 1.工作方式寄存器TMOD 工作方式寄存器TMO…