【Java】堆和优先级队列PriorityQueue

news2024/9/20 20:32:59

文章目录

  • 一、堆
    • 1.1 堆的概念
    • 1.2 堆的存储方式
    • 1.3 堆的创建
    • 1.4 堆的插入与删除
    • 1.5 堆的应用场景
  • 二、 优先级队列
    • 2.1 什么是优先级队列
    • 2.2 堆模拟实现优先级队列
  • 三、Java中的PriorityQueue
    • 3.1 PriorityQueue的特性
    • 3.2 常用方法


一、堆

1.1 堆的概念

在数据结构中,堆(Heap)是一种特殊的树形结构。它总是一颗完全二叉树,其父节点的值大于或等于(大堆)或者小于或者等于(小堆)其子节点的值。

例如下图所展示的小堆和大堆:

堆的主要特点是:

  • 在大堆中,根节点的值始终大于或等于其子节点的值;
  • 在小堆中,根节点的值始终小于或等于其子节点的值;

根据这种性质就能够帮助我们快速的找到堆中的最大值或者最小值。优先级队列就是有堆来实现的,其中元素的访问和删除操作都是基于元素的优先级。在大堆中,根节点始终是优先级最高的元素,可以快速找到和删除最大元素。同样,在小堆中,根节点是优先级最低的元素

1.2 堆的存储方式

从以上堆的概念可知,堆是一棵完全二叉树,因此可以采用数组,以层序遍历的顺序对元素进行储存


需要注意的是,对于非完全二叉树则不适合采用数组顺序存储,因为为了能够还原出二叉树,数组中还必须要存储空节点,这样势必就会浪费一定的空间了。

此外,将堆中的元素存储到数组之后,可可以根据二叉树的性质对树进行还原。假设 i 为节点在数组中的下标,则有:

  • 如果 i 为 0,则表示 i 下标的节点为根节点;若不为 0 ,则其父节点的下标为:(i - 1) / 2
  • 如果 2 * i + 1 小于总节点个数,则节点 i 的左孩子节点小标为 2 * i + 1,否则就没有左孩子节点。
  • 如果 2 * i + 2 小于总节点个数,则节点 i 的右孩子节点小标为 2 * i + 1,否则就没有右孩子节点。

1.3 堆的创建

1. 堆向下调整

对于数组{27, 15, 19, 18, 28, 34, 65, 49, 25, 37},如何使用数组中的元素建立一个小堆呢?

观察上图可以发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。

向下调整的过程:

  1. parent标记当前需要调整的节点下标,用child标记parent的左孩子下标(注意:parent如果有孩子一定先是有左孩子)。

  2. 如果parent的左孩子存在,即child < size, 进行以下操作,直到parent的左孩子不存在:

    • parent右孩子是否存在,如果存在,找到左右孩子中最小的孩子,用child进行标记;
    • parent与较小的孩子child进行比较,如果parent小于较小的孩子child,调整结束;否则交换parentchild节点,然后更新parentchildchild = 2 * parent + 1,继续向下调整。

例如下面的调整流程图:


即最后当child的值大于数组的长度length的时候就调整完成了。

向下调整算法的代码如下:

public void shiftDown(int[] array, int parent) {
    // child先标记parent的左孩子,因为parent可能右左没有右
    int child = 2 * parent + 1;
    int size = array.length;

    while (child < size) {

        // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
        if (child + 1 < size && array[child + 1] < array[child]) {
            child += 1;
        }

        // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
        if (array[parent] <= array[child]) {
            break;
        } else {
            // 将双亲与较小的孩子交换
            int tmp = array[parent];
            array[parent] = array[child];
            array[child] = tmp;

            // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
            parent = child;
            child = parent * 2 + 1;
        }
    }
}

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

2. 堆的创建

那对于普通的序列{ 1, 5, 3, 8, 7, 6 }要建立大堆,现在其根节点的左右子树不满足堆的特性,又该如何调整呢?

例如下面的流程图:

  1. 首先找到最后一个叶子节点的父节点,然后对其进行向下调整;
  2. 依次找当前父节点的前一个节点作为父节点,然后进行向下调整,直到找到根节点。

实现的代码如下:

public void createHeap(int[] array) {
    // 找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
    for (int i = (array.length - 1 - 1) / 2; i >= 0; i--){
        shiftDown(array, i);
    }
}

1.4 堆的插入与删除

1. 堆的插入

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

堆的向上调整算法如下:

    public void shiftUp(int child, int[] array) {
        // 找到child的双亲
        int parent = (child - 1) / 2;

        while (child > 0) {
            // 如果双亲比孩子大,parent满足堆的性质,调整结束
            if (array[parent] > array[child]) {
                break;
            } else {
                // 将双亲与孩子节点进行交换
                int t = array[parent];
                array[parent] = array[child];
                array[child] = t;

                // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
                child = parent;
                parent = (child - 1) / 2;
            }
        }
    }

2. 堆的删除

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

  1. 将堆顶元素对堆中最后一个元素交换
  2. 将堆中有效数据个数减少一个
  3. 对堆顶元素进行向下调整

1.5 堆的应用场景

1. 实现优先级队列

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

  1. 建堆

    • 升序:建大堆
    • 降序:建小堆
  2. 利用堆删除思想来进行排序

    • 建堆和堆删除中都用到了向下调整,因此利用向下调整,就可以完成堆排序。
  3. Top K 问题

例如:求最小K个元素

思路:

  • 要解决Top K 问题,不能简单的对数组进行排序,然后直接去前K个元素,因为如果数据量非常大的情况下,就不能将全部数据都加载进内存,也就不能实现排序了。
  • 因此就需要使用到优先级队列了。当然不是建立一个小堆,然后用全部的元素添加到堆中,最后弹出k个元素。那么这样的话时间复杂度就为O(N + K * logN) 了。
  • 这里解决的思路就是:如果要求最小的K个元素,那么就建立一个大小为K的大堆,然后将数组中的前K个元素添加到大堆中,再遍历数组中剩下的N - 1 个元素,当遇到比堆顶元素大的就与堆顶元素进行交换。当遍历完整个数组的时候,最小的前K个元素就保存到堆中的。
  • 这种方法的整个时间复杂度可以达到 O(N * logK),而 K 的值一般比较小。
class Solution{
    public int[] smallestK(int[] arr, int k) {
        
        int[] res = new int[k];
        // k 为 0 时,PriorityQueue的构造方法会抛异常
        if(arr == null || k == 0){
            return res;
        }

        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });

        for (int i = 0; i < arr.length; i++) {
            if(maxHeap.size() < k){
                maxHeap.offer(arr[i]);
            }
            else {
                if(arr[i] < maxHeap.peek()){
                    maxHeap.poll();
                    maxHeap.offer(arr[i]);
                }
            }
        }

        // System.out.println(maxHeap);

        for (int i = 0; i < k; i++) {
            res[i] = maxHeap.poll();
        }
        
        return res;
    }
}

二、 优先级队列

2.1 什么是优先级队列

优先队列(Priority Queue)是一种特殊的队列数据结构,其中每个元素都关联有一个优先级。与普通队列不同,优先队列中的元素并不按照插入的顺序进行处理,而是根据其优先级进行排序和访问。

优先队列的主要特点是,当从队列中取出元素时,具有最高优先级的元素首先被访问和删除。这意味着高优先级的元素先于低优先级的元素被处理。对于相同优先级的元素,可以根据插入的顺序或其他规则来确定其访问顺序。

优先队列可以通过不同的数据结构来实现,常见的实现方式包括堆(Heap)、二叉搜索树(Binary Search Tree)等。其中,使用堆实现的优先队列最为常见和高效。堆可以快速访问和删除具有最高(或最低)优先级的元素,并保持队列的有序性质。

优先队列的常见操作包括插入元素、删除最高优先级元素、获取最高优先级元素等。插入操作将元素按照其优先级插入到队列中的适当位置,删除操作会删除并返回具有最高优先级的元素,获取操作则返回但不删除最高优先级的元素。

优先队列在很多应用中都有广泛的应用,例如任务调度、图算法(如Dijkstra算法、Prim算法)等。它提供了一种方便的方式来处理具有优先级的元素,使得高优先级的任务或对象可以更快地被处理和访问。

2.2 堆模拟实现优先级队列

public class MyPriorityQueue {
    // 演示作用,不再考虑扩容部分的代码
    private int[] array = new int[100];
    private int size = 0;

    private void shiftUp(int child) {
        // 找到child的双亲
        int parent = (child - 1) / 2;

        while (child > 0) {
            // 如果双亲比孩子大,parent满足堆的性质,调整结束
            if (array[parent] > array[child]) {
                break;
            } else {
                // 将双亲与孩子节点进行交换
                int t = array[parent];
                array[parent] = array[child];
                array[child] = t;

                // 小的元素向下移动,可能到值子树不满足对的性质,因此需要继续向上调增
                child = parent;
                parent = (child - 1) / 2;
            }
        }
    }

    private void shiftDown(int parent) {
        // child先标记parent的左孩子,因为parent可能右左没有右
        int child = 2 * parent + 1;
        int size = array.length;

        while (child < size) {

            // 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
            if (child + 1 < size && array[child + 1] < array[child]) {
                child += 1;
            }

            // 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
            if (array[parent] <= array[child]) {
                break;
            } else {
                // 将双亲与较小的孩子交换
                int tmp = array[parent];
                array[parent] = array[child];
                array[child] = tmp;

                // parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
                parent = child;
                child = parent * 2 + 1;
            }
        }
    }


    public void offer(int e) {
        array[size++] = e;
        shiftUp(size - 1);
    }


    public int poll() {
        int oldValue = array[0];
        array[0] = array[--size];
        shiftDown(0);
        return oldValue;
    }

    public int peek() {
        return array[0];
    }
}

三、Java中的PriorityQueue

3.1 PriorityQueue的特性

在Java中,PriorityQueue 是一个实现了优先级队列的类。它是基于堆(Heap)数据结构实现的,具有以下特性:

  1. 优先级顺序:元素根据其优先级进行排序。默认情况下,元素按照自然顺序进行排序,或者可以通过提供自定义的比较器(Comparator)来定义元素的排序规则。

  2. 自动调整:当插入或删除元素时,PriorityQueue 会自动进行调整以保持堆的性质。插入元素时,会根据优先级将元素放入合适的位置;删除元素时,会移除堆顶元素,并重新调整堆以保持堆的性质。

  3. 快速访问:PriorityQueue 提供了快速访问具有最高(或最低)优先级的元素。通过 peek() 方法可以获取堆顶的元素,而不会删除它;通过 poll() 方法可以获取并删除堆顶的元素。

  4. 元素重复:PriorityQueue 允许元素重复。如果多个元素具有相同的优先级,它们的相对顺序可能会根据插入的顺序而不同。

  5. 动态大小:PriorityQueue 具有动态调整大小的能力。它会根据需要自动扩展或收缩底层数组的大小,以容纳更多或更少的元素。

需要注意的是,PriorityQueue 是非线程安全的,不适用于多线程环境。如果在多线程环境中使用,应该采取适当的同步措施。

PriorityQueue 是Java提供的一个常用的数据结构,广泛应用于任务调度、事件处理、最短路径算法等场景,方便地处理具有优先级的元素。

3.2 常用方法

PriorityQueue 实现了 Queue 接口并提供了一些额外的方法来支持优先级队列的操作。下面是一些常用的 PriorityQueue 方法:

  1. add(E element) / offer(E element):将指定元素插入队列。如果队列已满,add 方法会抛出异常,而 offer 方法会返回 false。
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(5);
pq.offer(3);
pq.offer(7);
  1. remove() / poll():移除并返回队列中的头部元素(具有最高优先级)。如果队列为空,remove 方法会抛出异常,而 poll 方法会返回 null。
int head = pq.remove();
int head = pq.poll();
  1. peek():返回队列中的头部元素(具有最高优先级),但不移除它。如果队列为空,返回 null。
int head = pq.peek();
  1. size():返回队列中的元素数量。
int size = pq.size();
  1. isEmpty():判断队列是否为空。
boolean empty = pq.isEmpty();

下面是一个完整的示例,演示了如何使用 PriorityQueue 来实现优先级队列:

import java.util.PriorityQueue;

public class PriorityQueueExample {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        
        pq.offer(5);
        pq.offer(3);
        pq.offer(7);
        pq.offer(1);
        
        while (!pq.isEmpty()) {
            int head = pq.poll();
            System.out.println("Element: " + head);
        }
    }
}

输出结果为:

Element: 1
Element: 3
Element: 5
Element: 7

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

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

相关文章

Python实现PSO粒子群优化算法优化随机森林回归模型(RandomForestRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 PSO是粒子群优化算法&#xff08;Particle Swarm Optimization&#xff09;的英文缩写&#xff0c;是一…

虚拟机的安装

1.选择自定义安装 然后下一步 2.选择稍后安装操作系统,也可以选择2 直接安装 3. 选择Linux和相关版本 4.命名虚拟机 默认都是C盘,修改一下好点 5.分配处理器 看需要同时开几台虚拟机,同时也看一下自己的CPU,处理器默认选1个就行,内核数量 选择为 逻辑处理器数量/同时开的虚…

easyupload

红框位置是上传后的提示 依次尝试上传&#xff0c;发现php,phtml.php3&#xff0c;php5&#xff0c;双写等都不行 .htaccess文件也不行 尝试.php. 绕过 (windows中会把后缀名最后的空格和. 省略&#xff0c;即上传.php.绕过后会变为.php) 加入图片头的php文件也不行 但是加入文…

Gitlab 新项目搭建

文章目录 Gitlab 新项目搭建新建空白项目初始化本地仓库并提交建立本地仓库和远程仓库关系并推送 Gitlab 新项目搭建 新建空白项目 项目名称与本地新建项目名称相同 初始化本地仓库并提交 进入本地项目根目录下&#xff0c;右击 git bash here打开命令窗口&#xff1b;初始化…

【Gradle】Gradle的概述与简单使用

一、概述 1.什么是Gradle&#xff1f; Gradle 是一种构建工具。 Java世界中主要有三大构建工具&#xff1a;Ant、Maven和Gradle。Ant几乎销声匿迹了&#xff0c;常见的就只有Maven和Gradle。 目前市面上Java开发&#xff0c;使用的构建工具基本都是Maven&#xff1b;安卓开…

Zabbix 的使用 续

Zabbix 的使用 续 一、部署 zabbix 代理服务器1.1 环境准备1.2 设置 zabbix 的下载源&#xff0c;安装 zabbix-proxy1.3 部署数据库&#xff0c;要求 MySQL 5.7 或 Mariadb 10.5 及以上版本1.4 导入数据库信息1.5 修改 zabbix-proxy 配置文件1.6 启动 zabbix-proxy1.7 在所有主…

Linux基础工具大全

今天&#xff0c;我带来Linux的基本工具大全。 目录 Linux 软件包管理器 yum软件包的概念查看软件包软件包的名称如何安装软件如何卸载软件更新yum源 Linux编辑器-vimvim的概念vim的几种模式vim的基本操作vim正常模式命令集vim末行模式命令集vim操作总结 Linux编译器-gcc/g的使…

AMCV761、AMC06电液伺服阀放大器

AMCV102、AMC106A、AMCV761、AMC06、AMC16、AMC12、AMC07、AMC13电液伺服阀双喷嘴一档板、干式力矩马达、力反馈八种基本形式&#xff0c;流量范围 1-400L/min、额定油压力 25Mpa 与服油缸或马达一起&#xff0c;用于位置、速度、加速度和力值控制。外置伺服放大器。型有抗污染…

C语言 指针进阶(一)

目录 一、字符指针 二、指针数组 通过类比的方法来认识指针数组 2.1指针数组的一般形式 2.2指针数组模拟实现二维数组 三、数组指针 通过类比的方法来认识数组指针 3.1数组指针的一般形式 3.2&数组名VS数组名 3.3数组指针的使用 四、数组参数、指针参数 4.1一维数…

Socket编程接口API并实现简单的TCP网络编程

#include <sys/types.h> #include <sys/socket.h> socket()创建套接字&#xff0c;成功返回套接字的文件描述符&#xff0c;失败返回-1 domain: 设置套接字的协议簇&#xff0c; AF_UNIX AF_INET AF_INET6 type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM prot…

云计算运维工程师需要会哪些技术

目前处于云年代&#xff0c;云计算运维工程师的工作远景还是十分广泛的。像是阿里云计算&#xff0c;滴滴&#xff0c;抖音等等互联网大厂目前都在使用云计算技能。 云计算运维工程师的薪资水平也十分可观。 运维工程师(Operations)&#xff0c;在国内又称为运维开发工程师(D…

一文弄懂Java日志框架

文章目录 日志的概念日志门面JUL日志框架JUL架构入门案例日志的级别Logger之间的父子关系日志的配置文件日志原理解析 LOG4J日志框架Log4j入门Log4j组件LoggersAppendersLayouts Layout的格式Appender的输出自定义Logger JCL日志门面JCL入门JCL原理 SLF4J日志门面SLF4J入门绑定…

风控策略基本功 | 授信额度(上)

通过调整额度&#xff0c;银行可以根据客户的实际情况和风险水平&#xff0c;灵活地匹配资金需求和风险承受能力。有助于确保风险控制、提高资金利用效率&#xff0c;并满足客户的需求。有效的额度调整能够避免过度或不足的授信&#xff0c;最大程度地降低违约风险&#xff0c;…

水印抹除算法总结

基本是从图片抹水印和视频抹水印两个方向 Video Inpainting&#xff1a;https://paperswithcode.com/task/video-inpaintingImage Inpainting&#xff1a;https://paperswithcode.com/task/image-inpainting 请根据目录查看 图片 Partial Conv 部分卷积层 源自于Image In…

Github Pages

官方教程&#xff1a;https://pages.github.com/ 1 创建仓库 命名为 你的名字.github.io 克隆项目 git clone https://github.com/username/username.github.io加入index.html页面 在克隆的项目中&#xff0c;加入一个index.html html文件简单写几个dom <!DOCTYPE html…

Redis - 附近商铺、用户签到、UV统计

文章目录 附近商铺、用户签到、UV统计一、附近商铺1.1 GEO数据结构1.2 导入店铺数据到GEO1.3 实现附近商户功能 二、用户签到2.1 BitMap2.2 签到功能2.3 统计连续签到2.3.1 分析2.3.2 代码实现 三、UV统计3.1 HyperLogLog用法3.2 测试百万数据的统计 附近商铺、用户签到、UV统计…

康复锻炼改善帕金森病人的功能障碍,你知道多少?快来学习!

帕金森病是一种神经系统退行性疾病&#xff0c;主要特征是肌肉僵硬、震颤和运动障碍等症状。虽然这个病无法彻底治愈&#xff0c;但通过康复锻炼可以显著改善患者的功能障碍。 首先&#xff0c;康复锻炼可以改善帕金森病人的肌肉僵硬。这种运动障碍是由于黑斑核和大脑皮层之间的…

RV1126笔记三十八:PaddleOCR部署到RV1126

若该文为原创文章&#xff0c;转载请注明原文出处。 一、环境 1、硬件&#xff1a;正点原子RV1126开发板 2、环境&#xff1a;ubuntu16.04 二、模型转换 训练后的模型不能直接使用在RV1126,需要转换一下模型 1、PaddlePaddle的模型转成推理模型 在前面有提过了&#xf…

三次样条曲线的偏移计算方法比较

最近&#xff0c;我不得不想出一种方法&#xff0c;从三次 Hermite 样条曲线创建平行曲线&#xff0c;例如铁路车道。 首先&#xff0c;我只是沿着法线方向移动它们的开始/结束控制点&#xff0c;同时保持相同的开始/结束切线。 它在大多数情况下工作得很好&#xff0c;因为我的…

Qt画图框架,实现自己的画图框架

前面也讲到Qt提供画图框架&#xff0c;经典MVC模型&#xff0c;也没有什么问题。但Qt仅提供框架性东西&#xff0c;很难落地&#xff0c;很难应用在实际项目当中&#xff0c;一般需要自己捋一遍&#xff0c;这样才能理解好 什么view&#xff0c;canvas都好理解&#xff0c;只要…