数据结构——优先级队列(堆)Priority Queue详解

news2024/11/26 14:36:28

1. 优先级队列

队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该场景下,使用队列不合适

在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象,这种数据结构就是优先级队列(Priority Queue)

2. 优先级队列的模拟实现

2.1 堆的概念

性质:

如上图,堆中某个节点的值总是不大于其父节点的值,称为最小堆/小根堆

堆中某个节点的值总是不小于其父节点的值,称为最大堆/大根堆

堆总是一棵完全二叉树

2.2 堆的存储方式

从堆的概念可知,堆是一棵完全二叉树,因此可用层序的规则采用顺序的方式进行高效存储

tip:对于非完全二叉树则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节点,就会导致空间利用率较低

将元素存储到数组中后,假设 i 为节点在数组中的下标,则有:

  • 如果 i 为0,则 i 表示的节点为根节点,否则 i 节点的双亲结点为(i - 1)/2
  • 如果 2*i+1 小于节点个数,则节点 i 的左孩子下标为 2*i+1 ,否则没有左孩子
  • 如果 2*i+2 小于节点个数,则节点 i 的右孩子下标为 2*i+2 ,否则没有右孩子

2.3 堆的创建

2.3.1 堆向下调整

以集合{ 27,15,19,18,28,34,65,49,25,37 }为例,转换为大根堆的过程:

//TestHeap 类
public class TestHeap {
    public int[] elem;//堆由数组实现
    public int usedSize;//记录有效数据个数

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

    public void init(int[] array) {//给数组初始化
        for (int i = 0; i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }

    //把elem数组中的数据调整为大根堆
    public void createHeap() {
        //让parent初始位置在树的最后一棵子树的根节点处,用到公式:根节点 = (i-1)/2
        //其中usedSize-1表示最后一个节点,从上到下循环遍历整棵树
        for (int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            //siftDown方法用来判断树中左右孩子与父亲节点的大小关系
            siftDown(parent,usedSize);
        }
    }

    //交换函数
    public void swap(int i, int j) {
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }
    public void siftDown(int parent, int end) {
        int child = 2*parent+1;//公式:左孩子下标 = 2*根节点下标+1
        while(child < end) {//end是有效数据个数,不能用<=
            //下面if走完之后,child一定是左右孩子中最大值的下标
            if(child+1 < end && elem[child] < elem[child+1]) {
                child++;
            }
            //下面if判断孩子节点是否大于父亲节点,若大于则交换,否则跳出循环,去判断上一棵树
            if(elem[child] > parent) {
                swap(child,parent);
                parent = child;
                child = 2*parent+1;
            }else {
                break;
            }
        }
    }
}

//Test 类
public class Test {
    public static void main(String[] args) {
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        TestHeap testHeap = new TestHeap();
        testHeap.init(array);
        testHeap.createHeap();
    }
}

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

时间复杂度:最坏的情况就如上例,从根一路比较到叶子,比较的次数为完全二叉树的高度,即时间复杂度为O(\log _{2}N)

2.3.2 建堆的时间复杂度

推导:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了方便计算,使用满二叉树来推导

结论: 建堆的时间复杂度为 O(N)

2.4 堆的插入与删除

2.4.1 堆的插入

堆的插入需要两步

1. 先将元素放到树底层最后一个节点(空间不够时扩容)

2. 将插入新节点向上调整,直到满足堆的性质

    public void offer(int val) {
        if(isFull()) {//判满,若满则扩容
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize] = val;//val赋值到usedSize位置,usedSize++
        usedSize++;
        siftUp(usedSize-1);//向上调整
    }
    public void siftUp(int child) {
        int parent = (child-1)/2;//通过孩子节点找到父亲节点
        while(parent >= 0) {//当父亲节点下标小于0,说明向上调整结束了,堆已有序
            if(elem[child] > elem[parent]) {//孩子节点的值大于父亲节点值,即交换
                swap(child,parent);
                child = parent;
                parent = (child-1)/2;
            }else {//若孩子节点的值小于父亲节点的值,说明堆已有序,直接跳出循环
                break;
            }
        }
    }
    public boolean isFull() {
        return usedSize == elem.length;
    }

2.4.2 堆的删除

tip:堆的删除一定删除的是堆顶元素,分为三步:

1. 将堆定元素对堆中最后一个元素交换

2. 将堆中有效数据个数减少一个

3. 对堆顶元素进行向下调整

    public int poll() {
        if(isEmpty()) {//若栈空,返回-1
            return -1;
        }
        int old = elem[0];//记录要删除的数据,最后返回
        swap(0,usedSize-1);//交换堆顶元素和最后元素
        usedSize--;//有效个数--
        siftDown(0,usedSize);//从堆定开始,向下调整
        return old;
    }
    public boolean isEmpty() {
        return usedSize == 0;
    }

例题:

1.下列关键字序列为堆的是:()

A: 100,60,70,50,32,65   B: 60,70,65,50,32,100   C: 65,100,70,32,50,60

D: 70,65,100,32,50,60   E: 32,50,100,70,65,60   F: 50,100,70,65,60,32

选A

2.已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()

A: 1         B: 2          C: 3        D: 4

解析:选C,15与10比,12与10比,12与16比

3.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A: [3,2,5,7,4,6,8]        B: [2,3,5,7,4,6,8]

C: [2,3,4,5,7,8,6]        D: [2,3,4,5,6,7,8]

选C

3. 常用接口介绍

3.1 PriorityQueue的特性

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

    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();

        priorityQueue.offer(1);
        priorityQueue.offer(2);
        priorityQueue.offer(3);

        System.out.println(priorityQueue.poll());//运行结果:1
        System.out.println(priorityQueue.poll());//        2
    }

关于PriorityQueue的使用注意:

  • 1.使用时必须导入PriorityQueue所在的包,即:import java.util.PriorityQueue;
  • 2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常 
  • 3. 不能插入null对象,否则会抛出 NullPointerException
  • 4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 5. 插入和删除元素的时间复杂度为O(log_2N)
  • 6. PriorityQueue底层使用了堆数据结构
  • 7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素

3.2 PriorityQueue常用接口介绍

3.2.1 优先级队列的构造

此处只是常见的几种:

构造器功能介绍
PriorityQueue()创建一个空的优先级队列,默认容量是11
PriorityQueue(int initialCapacity)创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常
PriorityQueue(Collection c)用一个集合来创建优先级队列

构造方法原理:

用一个集合来创建优先级队列:

class Student {

}
public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);
        arrayList.add(2);
        arrayList.add(3);
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(arrayList);

        priorityQueue.offer(4);
        priorityQueue.offer(5);
        priorityQueue.offer(6);

        System.out.println(priorityQueue.poll());//运行结果:1
        System.out.println(priorityQueue.poll());//        2

        PriorityQueue<Student> priorityQueue1 = new PriorityQueue<>(arrayList);//error
        //传入参数必须是Student类或其子类
    }
}

3.2.2 offer元素原理:

查看源码:

结合上面创建小根堆的流程分析:

自己实现比较器:

//实现小根堆的比较器
class IntCmp implements Comparator<Integer> {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);//小根堆
    }
}
public class Test {
    public static void main(String[] args) {
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new IntCmp());//别忘了此处new比较器对象作为参数

        priorityQueue.offer(4);
        priorityQueue.offer(5);
        priorityQueue.offer(6);

        System.out.println(priorityQueue.poll());//运行结果:4
        System.out.println(priorityQueue.poll());//        5
    }
}

//实现大根堆的比较器
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> priorityQueue = new PriorityQueue<>(new IntCmp());

        priorityQueue.offer(4);
        priorityQueue.offer(5);
        priorityQueue.offer(6);

        System.out.println(priorityQueue.poll());//运行结果:6
        System.out.println(priorityQueue.poll());//        5
    }

3.2.3 PriorityQueue的扩容方式

以下是JDK17中的源码:

说明:

如果容量小于64时,按照oldCapacity的2倍方式扩容

如果容量大于等于64,按照oldCapacity的1.5倍方式扩容

如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容

3.3 OJ练习

top-k问题:最大或最小的前k个数据

    public static int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        for (int i = 0; i < arr.length; i++) {
            minHeap.offer(arr[i]);
        }
        int[] tmp = new int[k];
        for (int i = 0; i < k; i++) {
            int val = minHeap.poll();
            tmp[i] = val;
        }
        return tmp;
    }

    public static void main(String[] args) {
        int[] array = {27,15,19,18,28};
        int[] ret = smallestK(array,3);
        System.out.println(Arrays.toString(ret));
    }

运行结果:

虽然上述方法也能满足需求,但是其时间复杂度为O((N+K)*\log _{2}N)

不是非常好的解决方案,现需要一个时间复杂度更小的方法:

若求前K个最小的数字

1. 先把前 个元素建立大小为K的大根堆

2. 遍历剩余 N-K 个元素,若堆顶元素大于当前 i 下标的值就出堆

这样遍历完数组后,大小为 K 的堆中就是最小的 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) {
        int[] tmp = new int[k];
        if(k == 0) {
            return tmp;
        }
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new IntCmp());
        //1.把前K个元素放进堆中
        for (int i = 0; i < k; i++) {
            maxHeap.offer(arr[i]);
        }
        //2.遍历剩下的 N-k 个元素
        for (int i = k; i < arr.length; i++) {
            int val = maxHeap.peek();
            if(val > arr[i]) {
                maxHeap.poll();
                maxHeap.offer(arr[i]);
            }
        }
        //3.将堆中元素放到数组中
        for (int i = 0; i < k; i++) {
            tmp[i] = maxHeap.poll();
        }
        return tmp;
    }
}

变种题:求第K小的数据,建立大根堆,堆顶元素就是第K小

3.4 堆排序

要求:在原堆上修改,不能新建

方法两步:

1. 建堆:

  • 要求升序,建大堆
  • 要求降序,建小堆

2. 利用堆删除来进行排序

建堆和堆删除中都用到了向下调整

    //堆排序 升序
    public void heapSort() {
        int endIndex = usedSize-1;
        while(endIndex > 0) {
            swap(0,endIndex);
            siftDown(0,endIndex);
            endIndex--;
        }
    }

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

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

相关文章

python-爬虫篇-爬取百度贴吧,段友之家的图片和视频

#!/usr/bin/env python # -*- coding: utf-8 -*-""" 爬取百度贴吧&#xff0c;段友之家的图片和视频 author: cuizy time&#xff1a;2018-05-19 """import requests import bs4 import osdef write_file(file_url, file_type):""&quo…

高可用设备日志采集工具

免费试用下载: Gitee下载 最新版本 优势: A. 开箱即用. 解压直接运行.不需额外安装. B. 批管理设备. 设备配置均在后台管理. C. 无人值守 客户端自启动,自更新. D. 稳定安全. 架构简单,内存占用小,通过授权访问.

Java——包

一、包 1、简要介绍 在Java编程语言中&#xff0c;包&#xff08;Package&#xff09; 是一种用来组织和管理类&#xff08;Class&#xff09;和接口&#xff08;Interface&#xff09;的机制。包为开发者提供了一种逻辑分组的方式&#xff0c;使代码更加模块化、结构化和易于…

获取泛型,泛型擦除,TypeReference 原理分析

说明 author blog.jellyfishmix.com / JellyfishMIX - githubLICENSE GPL-2.0 获取泛型&#xff0c;泛型擦除 下图中示例代码是一个工具类用于生成 csv 文件&#xff0c;需要拿到数据的类型&#xff0c;使用反射感知数据类型的字段&#xff0c;来填充表字段名。可以看到泛型…

开关阀(3):Fisher DVC6200定位器原理及调试

Fisher DVC6200---Digital Valve Controllers&#xff08; 数字阀门控制器&#xff09;简写 DVC,而6200是Fisher DVC定位器发展的一个系列型号&#xff0c;是Fisher结合DVC2000、DVC6000系列&#xff0c;取其特点发展的有着高适用性和高可靠性的阀门定位器。 DVC6200 原理&…

企业智慧办公管理平台

摘要 在之前的疫情中&#xff0c;大多数企业都受到了较大的冲击&#xff0c;然而一些公司却因为工作的特殊性可以居家远程办公&#xff0c;不过这些企业在管理员工的过程中却遇到了较大的困难&#xff0c;这是因为这些企业的管理系统根本大多都无法管理员工的工作项目&#xf…

【启明智显分享】国产工业级HMI芯片Model3C——个位数价,双CAN配置

在工业自动化、汽车控制系统中&#xff0c;通信技术的选择至关重要。其中&#xff0c;CAN&#xff08;Controller Area Network&#xff09;通信协议以其高实时性、可靠性和灵活性&#xff0c;已成为这些领域的首选。 从单CAN到双CAN&#xff1a;双重保障效率翻倍 CAN是一种多…

GNN Algorithms(8): DDPM

扩散模型 diffusion model&#xff1a;正向扩散过程 Forward Diffusion Process、反向生成过程 Reverse Generation Process. 本质&#xff1a;DDPM, Denoising Diffusion Probabilistic Model T steps 加噪&#xff1a;没有参数&#xff0c; ->随机取一个每一步都要加的随…

基于SSM+Jsp的体育竞赛成绩管理系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

Emacs之保存时删除行尾空格(一百四十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

【机器学习】机器学习赋能交通出行:智能化实践与创新应用探索

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀目录 &#x1f4d2;1. 引言&#x1f4d9;2. 交通流量预测与优化&#x1f31e;数据准备&#x1f319;模型训练与预测⭐评估模型与优化 &#x…

Linux操作系统篇:多线程

一. Linux中线程是怎么理解的 1.1 线程概念 在Linux中&#xff0c;线程是在进程“内部”执行的&#xff0c;线程是处于进程的进程地址空间中运行&#xff0c;线程用到的资源都是进程的资源&#xff0c;线程是执行进程的一部分代码&#xff0c;线程是最小的执行流&am…

C++代码编写风格:Header-Only与声明实现分离的选择

C代码编写风格&#xff1a;Header-Only与声明实现分离的选择 最近看到一些小伙伴问到了几个比较有趣的问题&#xff0c;这里总结一下&#xff0c;这些都是实际面试中出现过的问题&#xff0c;看看你知道多少&#xff0c;考察一下底子。 面试问题1&#xff1a;你通常编写代码的风…

eNSP学习——配置基于全局地址池的DHCP

目录 主要命令 原理概述 实验目的 实验场景 实验拓扑 实验编址 实验步骤 1、基本配置 2、配置基于全局地址池的 DHCP Server 3、配置DHCP Client 主要命令 [R1]dhcp enable //开启 DHCP功能//创建一个全局地址池&#xff0c;地址池名称为huawei1 [R1]ip pool h…

RIP路由协议汇总、版本兼容、定时器、协议优先级配置(华为)

#交换设备 RIP路由协议汇总 一、原理概述 当网络中路由器的路由条目非常多时&#xff0c;可以通过路由汇总&#xff08;又称路由汇聚或路由聚合&#xff09;来减少路由条目数&#xff0c;加快路由收敛时间和增强网络稳定性。路由汇总的原理是&#xff0c;同一个自然网段内的…

linux挂载硬盘(解决linux不显示硬盘问题)

目录 1.查看系统有几块硬盘2.查看挂载情况3.格式化硬盘4.创建挂载目录用于挂载硬盘5.将硬盘挂载到指定的挂载目录6.随系统自启动挂载查看配置文件&#xff0c;看是否已经把这条命令加入配置 帮同门解决挂载失败问题记录 参考视频&#xff1a;只要6步&#xff01;Linux系统下挂载…

内容安全复习 10 - 异常检测

文章目录 概述什么是异常检测异常检测应用与二分类分类器的辨析广义分布外检测&#xff08;OOD&#xff09; 异常检测分类Deep Learning for Feature Extraction&#xff08;用于特征提取的深度学习&#xff09;Learning Feature Representations of Normality&#xff08;学习…

人工智能在影像组学与放射组学中的最新进展|顶刊速递·24-06-22

小罗碎碎念 本期文献速递的主题——人工智能在影像组学中的最新进展。 小罗一直以来的观点&#xff0c;是把大问题分模块拆解——既然我们想做多模态&#xff0c;那么就先了解单模态的研究套路&#xff0c;再去研究不同模态提取的特征如何融合&#xff0c;搞科研的过程也是管理…

R语言数据分析案例32-针对芬兰污染指数的分析与考察

一、 研究背景及意义 近年来&#xff0c;随着我国科技和经济高速发展&#xff0c;人们生活质量也随之显著提高。但是&#xff0c; 环境污染问题也日趋严重&#xff0c;给人们的生活质量和社会生产的各个方面都造成了许多不 利的影响。空气污染作为环境污染主要方面&#xff0c…

汽车销售系统

摘 要 在现代社会&#xff0c;电脑是企业运作和管理必不可少的工具。我们过去用手记下卖出的商品的年代已一去不复返了。在我国&#xff0c;汽车销售行业的竞争日趋激烈的情况下&#xff0c;如何提高企业的管理水平&#xff0c;提高企业的工作效率&#xff0c;提高企业的服务质…