数据结构(Java):优先级队列(堆)堆的模拟实现PriorityQueue集合

news2024/12/23 17:19:30

目录

1、优先级队列

1.1 概念

1.2 PriorityQueue底层结构

2、 堆

2.1 堆的概念

 2.2 堆的存储结构

3、优先级队列(堆)的模拟实现

3.1 堆的创建

3.1.1 向下调整算法建完整堆

3.2 堆的插入 

3.2.1 向上调整算法

 3.3 堆的删除

3.4 堆排序

4、PriorityQueue集合

 4.1 PriorityQueue的特点

4.2  PriorityQueue的方法

 4.3 PriorityQueue源码分析

4.3.1 成员变量

 4.3.2 构造方法

4.3.3 插入方法(offer)

 4.3.4 扩容方法(扩容机制)

 4.4 TOP-K问题 

4.4.1 TOP-K问题 力扣OJ题 


1、优先级队列

1.1 概念

对于队列而言,数据只能从队尾进,队头出,遵循着固定的先进先出原则。

而在某些特殊场景需求下,要求优先级高的元素先出队列,

在这种情况下,数据结构应该提供两个最基本的操作:

  • ①:不仅能添加新对象
  • ②:还能返回最高优先级对象。

这种数据结构就是优先级队列,Java也提供了PriorityQueue集合。 

1.2 PriorityQueue底层结构

PriorityQueue底层是堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整,接下来,让我们来聊一聊堆到底是什么。


2、 堆

2.1 堆的概念

 所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。

堆分为 大根堆与小根堆。

大根堆:每个节点都大于或等于其子节点。

小根堆:每个节点都小于或等于其子节点。


 2.2 堆的存储结构

我们已知堆为一棵完全二叉树,故采用顺序的方式存储在数组中。

而对于我们之前所学的二叉树,为什么没有采用顺序存储而是采用链式存储呢?

因为二叉树并非都为完全二叉树,若采用顺序存储会造成空间浪费:

因为堆采用顺序的方式进行存储,且为完全二叉树,故具有以下性质:

  1. 孩子节点下标为i,则其父亲节点下标为(i - 1)/2
  2. 父亲下标为i,则其左孩子下标为2i+1(2i+1<节点总数的情况下,否则无左孩子)
  3. 父亲下标为i,则其右孩子下标为2i+2(2i+2<节点总数的情况下,否则无右孩子)

3、优先级队列(堆)的模拟实现

注:下文中代码实现的为大根堆

3.1 堆的创建

 向下调整算法

向下调整算法:选出其左右孩子中值较小值元素(建大堆就选较大元素,建小堆就选较小元素,这里以建小堆为例),将这个元素和根节点进行比较,若比根节点还小,就和根节点交换,交换后可能导致子树不满足堆的性质,因此需要继续向下调整。

注意:向下调整算法的使用,必须要求其左右子树必须为大根堆或者小根堆!!!

向下调整算法的时间复杂度为:O(logN),因为最坏情况是从根一路比较到叶子,比较的次数即为完全二叉树的高度次。

3.1.1 向下调整算法建完整堆

我们给出一组数据:{ 27,15,19,18,28,34,65,49,25,37 },要想将这组数据建成堆,我们可以使用向下调整法建堆。

而一组乱序数据其左右子树是不为堆的,那就需要通过向下调整算法从倒数第一个非叶子节点(下标为(数组.length-1-1)/2 )开始建堆,直到将整棵树建成堆。

建堆的时间复杂度为:O(N)!!!,

什么不是O(N*logN)呢?这里给出解释:

向下调整整体建堆代码:

/**
     * 建堆整体时间复杂度:O(N)
     */
    public void createHeap() {
        //从倒数第一个飞非叶子节点开始建堆
        int parent = (this.usedSize - 1 - 1)/2;
        while (parent >= 0) {//O(N)
            siftDown(parent,this.usedSize);
            parent--;
        }
    }
    /**
     *向下调整算法
     * 时间复杂度:O(logN)
     * @param parent 向下调整的起始位置,即根节点
     * @param usedSize 标定调整的结束 若算出的孩子节点坐标>=usedSize时,说明已调整完成
     */
    private void siftDown(int parent, int usedSize) {
        int child = parent*2+1;
        //当没有孩子节点时,说明向下调整完成
        while (child < usedSize) {
            //当右孩子存在时,选出左右孩子的最大值(建大堆)
            if (child + 1 < usedSize) {
                if (elem[child] < elem[child + 1]) {
                    child = child + 1;
                }
            }
            //和根节点进行比较
            if (elem[child] > elem[parent]) {
                swap(elem, parent, child);
                parent = child;
                child = parent*2+1;
            }else {
                //若根节点大,说明已是大堆,break结束
                break;
            }
        }
    }

3.2 堆的插入 

插入数据总共有两个步骤:

  1. 将新元素插入堆的末尾(插入后不再为堆)
  2. 使用向上调整算法调整为堆 

3.2.1 向上调整算法

(这里以建小堆为例):将元素插入到堆的末尾后,和根节点进行比较,如果比根节点还小就和根节点换,继续向上调整,直至满足堆的性质。如果没有根节点小,说明此时已满足堆的性质,调整完成。

时间复杂度:O(logN)

插入元素代码:

/**
     * 插入元素
     * 时间复杂度:O(logN)
     * @param x:新插入元素的值
     */
    public void offer(int x) {
        if (isFull()) {
            grow();
        }
        elem[usedSize] = x;
        siftUp(usedSize);
        usedSize++;
    }

    /**
     * 向下调整算法
     * @param childIndex:新插入元素的下标
     */
    public void siftUp(int childIndex) {
        //找到新节点的父节点的下标
        int parent = (childIndex - 1)/2;
        //parent为负数时,说明调整完成(最坏)
        while (parent >= 0) {
            if (elem[parent] < elem[childIndex]) {
                swap(elem,parent,childIndex);
                childIndex = parent;
                parent = (childIndex - 1)/2;
            }else {
                break;
            }
        }
    }
    /**
     * 如果堆底层的数组满了,就扩容
     */
    private void grow() {
        this.elem = Arrays.copyOf(elem,elem.length*2);
    }

向上调整算法建堆【拓展】 

故我们也可以一个一个的插入元素(使用向上调整算法)来建堆,但是不推荐,因为时间复杂度比向下调整算法建堆要大,为:O(N*logN)

其实主要原因就是:最后一层的元素是最多的,都要一个个向上调整


 3.3 堆的删除

堆的删除一定删除的是堆顶元素。

故,删除元素的思想很简单,即:

  1.  将堆顶元素和堆中最后一个元素交换
  2.  将堆中有效数据个数(usedSize)减一
  3.  对堆顶元素进行向下调整(只需要调整0下标就可以了)

 堆删除代码:

/**
     * 删除堆元素
     */
    public void poll() {
        if (isEmpty()) {
            //如果堆为空,返回
            return;
        }
        //交换堆顶和最后一个元素
        swap(elem,0,usedSize-1);
        //堆元素有效个数-1
        usedSize--;
        //只向下调整0下标即可
        siftDown(0,usedSize);
    }

3.4 堆排序

若要升序排列,要建大根堆;若要降序排列,就要建小根堆。

以排升序为例:若要排升序,则为大堆,排序过程如下:

  1. 将堆顶元素和堆末元素交换,有效数据个数减一(因为堆顶元素为最大值元素,此时最大值元素已来到数组末尾)
  2. 将0下标处向下调整,重新调整为大堆
  3. 继续将堆顶元素和堆末元素交换,有效数据个数减一(堆顶元素为次大值元素,此时次大值元素已来到数组末尾倒数二个位置处)
  4. 将0下标处向下调整,重新调整为大堆
  5. 重复以上过程,直至只剩一个元素的时候,此时数组已有序且为升序排列

堆排序 排升序代码 :

/**
     * 堆排序
     */
    public void heapSort() {
        if (isEmpty()) {
            return;
        }
        int end = usedSize-1;
        while (end > 0) {
            swap(elem,0,end);
            siftDown(0,end);
            end--;
        }
    }

4、PriorityQueue集合

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

 4.1 PriorityQueue的特点

  1. 我们知道,堆的建立和每次的插入删除数据都需要需要对堆进行调整,意味着要有可比较大小的功能。PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象(自定义类要实现Comparable接口或者提供比较器),否则会抛出ClassCastException异常。
  2. 不能插入null对象,否则会抛出NullPointerException
  3. 当元素满时,会自动扩容
  4. 插入和删除元素的时间复杂度为O(logN)(删除要向下调整,插入要向上调整,但最多调整高度次)
  5. PriorityQueue底层使用了堆数据结构
  6. PriorityQueue默认情况下是小堆---即每次获取的堆顶元素都是最小的元素

4.2  PriorityQueue的方法

 4.3 PriorityQueue源码分析

4.3.1 成员变量

PriorityQueue的底层是堆,使用的是顺序结构来存储数据。

 4.3.2 构造方法

 以上的三个构造方法,实际上调用的就是带两个参数的构造方法:

  1. 第一个参数是容量的大小,若没有指定容量,那默认容量就是11
  2. 第二个参数是比较器,用户可以自定义比较器:直接实现Comparator接口,然后重写该接口中的compare方法,给出比较的功能,若没有提供那就是null

 当然,我们也可以用集合传参来构造优先级队列:

4.3.3 插入方法(offer)

我们先来分析源码:

 通过观察offer方法(插入数据)的底层源码,我们得出结论:

  • ①:向上调整时,如果我们提供了比较器,就通过比较器比较;如果没有提供比较器,就通过Comparable接口的方法进行比较(比较器和Comparable两者必有其一)
  • ②:建堆时默认建的是小堆,如果要建大堆,我们可以修改比较器或者Comparable接口中compareTo方法的内部实现

上图所分析的是使用Comparable中的方法进行比较,若使用比较器来进行比较的话,内部思想其实是一模一样的。

 4.3.4 扩容方法(扩容机制)

  •  如果容量小于64时,是按照oldCapacity的2倍方式扩容的
  • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
  • 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容(最多就是MAX_ARRAY_SIZE)

 4.4 TOP-K问题 

TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

目前解决这个问题我们有三种思路,设共有N个数据:

  1. 将全部N个元素排序,取前K个元素
  2.  将整体N个元素建成堆,拿出堆顶元素,调整;再拿出堆顶元素,再调整......依次拿出K个元素
  3. (求前K个最小元素):把前K个元素建成大根堆,将堆顶元素依次和剩下的N-K个元素比较,若比堆顶元素小,就删除堆顶元素并将这个元素入堆,将剩余的元素全部比较完后,堆中的K个元素就是前K个最小元素,堆顶元素就是第K个最小元素。(建大根堆的原因就是,堆顶元素是堆中最大的,和其余N-K个元素比较后,堆中元素就是所有元素中最小的,而堆顶元素就是第K小元素)

我们分析以上三种思路的时间复杂度:

思路一:最快的排序算法也只能为:O(N*logN)

思路二:使用集合时,我们只能使用offer方法插入元素向上调整来建堆, 为O(N*logN),接着依次出K个元素,每次出完都要调整,为O(K*logN),故整体为O((N+K)*logN)

思路三:前K个元素建堆的时间复杂度为:O(K*logK),剩余N-K个元素依次比较和调整为堆的时间复杂度为:O((N-K)*logK),故整体为:O(N*logK)

我们可以看出思路三的时间效率是最高的,所以我们可以使用思路三来解决遇见的TOP-K问题。 

思路三总结: 

  • 求前K个最大元素,建小堆。
  • 求前K个最小元素,建大堆。

4.4.1 TOP-K问题 力扣OJ题 

 . - 力扣(LeetCode)

代码: 

/**
 * 最小前K个数 ->建大堆
 */
class Solution {
    public int[] smallestK(int[] arr, int k) {
        PriorityQueue<Integer> priorityQueue = 
                new PriorityQueue<>(new Comparator<Integer>() {
            //匿名内部类 建大堆-》修改比较器方法
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        int[] ret = new int[k];
        if(arr == null || k == 0) {
            return ret;
        }
        for (int i = 0; i < k; i++) {
            priorityQueue.offer(arr[i]);
        }
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < priorityQueue.peek()) {
                priorityQueue.poll();
                priorityQueue.offer(arr[i]);
            }
        }
        for (int i = 0; i < k; i++) {
            ret[i] = priorityQueue.poll();
        }
        return ret;
    }
}

OK~本次博客到这里就结束了,

感谢大家的阅读~欢迎大家在评论区交流问题~

如果博客出现错误可以提在评论区~

创作不易,请大家多多支持~

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

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

相关文章

Java实现随机题库-全站最呆瓜思想,保证你能学会

目录 Blue留言 &#xff1a; 学本篇文章之前所需掌握技能 推荐视频&#xff1a; 代码逻辑思想 步骤&#xff1a; 1、引入依赖 2、做一个excel表格 3、java实现从excel表中取数据 第一步&#xff1a;根据excel上面的字段名&#xff08;如下图&#xff09;&#xff0…

Python应用开发——30天学习Streamlit Python包进行APP的构建(18):定制组件

Custom components定制组件 st.components.v1.declare_component 创建自定义组件,并在有 ScriptRunContext 的情况下进行注册。 如果没有 ScriptRunContext,则不会注册该组件。当自定义组件作为独立命令执行时(如用于测试),可能会出现这种情况。 要使用该函数,请从 s…

网络安全常用易混术语定义与解读(Top 20)

没有网络安全就没有国家安全&#xff0c;网络安全已成为每个人都重视的话题。随着技术的飞速发展&#xff0c;各种网络攻击手段层出不穷&#xff0c;保护个人和企业的信息安全显得尤为重要。然而&#xff0c;在这个复杂的领域中&#xff0c;许多专业术语往往让人感到困惑。为了…

分布式系列之ID生成器

背景 在分布式系统中&#xff0c;当数据库数据量达到一定量级后&#xff0c;需要进行数据拆分、分库分表操作&#xff0c;传统使用方式的数据库自有的自增特性产生的主键ID已不能满足拆分的需求&#xff0c;它只能保证在单个表中唯一&#xff0c;所以需要一个在分布式环境下都…

JavaScript进阶之作用域解构箭头函数

目录 一、作用域1.1 局部作用域1.2 全局作用域1.3 作用域链1.4 垃圾回收机制1.5 闭包1.6 变量提升 二、函数进阶2.1 函数提升2.2 函数参数2.3 箭头函数&#xff08;重要&#xff09; 三、解构赋值3.1 数组解构3.2 对象解构&#xff08;重要重要&#xff09; 一、作用域 1.1 局…

全自动蛋托清洗机介绍:

全自动蛋托清洗机&#xff0c;作为现代蛋品处理设备的杰出代表&#xff0c;凭借其高效、智能、环保的特性&#xff0c;正逐步成为蛋品加工行业的得力助手。 这款清洗机采用了先进的自动化设计理念&#xff0c;从进料、清洗到出料&#xff0c;全程无需人工干预&#xff0c;极大…

SpringCloud---服务注册(Eureka)

目录 前言 一.注册中心 二.CAP理论 三.常见的注册中心 四.Eureka 4.1搭建Eueka Server 4.2服务注册 4.3发现服务 4.4小结 学习专栏&#xff1a;http://t.csdnimg.cn/tntwg 前言 在SpringCloud里&#xff0c;我们可以发现一个巨大的问题&#xff0c;就是url是写死的&am…

如何在 Android 中删除和恢复照片

对于智能手机用户来说&#xff0c;相机几乎已经成为一种条件反射&#xff1a;你看到值得注意的东西&#xff0c;就拍下来&#xff0c;然后永远保留这段记忆。但如果那张照片不值得永远保留怎么办&#xff1f;众所周知&#xff0c;纸质快照拿在手里很难舍弃&#xff0c;而 Andro…

grafana大坑,es找不到时间戳 | No date field named timestamp found

grafana大坑&#xff0c;es找不到时间戳。最近我这边的es重新装了一遍&#xff0c;结果发现grafana连不上elasticsearch了&#xff08;以下简称es&#xff09;&#xff0c;排查问题查了好久一直以为是es没有装成功或者两边的版本不兼容&#xff0c;最后才发现是数值类型问题 一…

一天搞定React(3)——Hoots组件

Hello&#xff01;大家好&#xff0c;今天带来的是React前端JS库的学习&#xff0c;课程来自黑马的往期课程&#xff0c;具体连接地址我也没有找到&#xff0c;大家可以广搜巡查一下&#xff0c;但是总体来说&#xff0c;这套课程教学质量非常高&#xff0c;每个知识点都有一个…

【Node】npm i --legacy-peer-deps,解决依赖冲突问题

文章目录 &#x1f356; 前言&#x1f3b6; 一、问题描述✨二、代码展示&#x1f3c0;三、运行结果&#x1f3c6;四、知识点提示 &#x1f356; 前言 npm i --legacy-peer-deps&#xff0c;解决依赖冲突问题 &#x1f3b6; 一、问题描述 node执行安装指令时出现报错&#xff…

【QT】label适应图片(QImage)大小;图片适应label大小

目录 0.简介 1.详细代码 1&#xff09;label适应img大小 2&#xff09;img适应label大小 0.简介 一个小demo &#xff0c;想在QLabel中放一张QImage的图片&#xff0c;我有一张图片叫【bird.jpg】&#xff0c;是提前放在资源文件中的&#xff0c;直接显示在label上后&#…

【网络】网络聊天室udp

网络聊天室udp 一、低耦合度代码1、代码2、测试结果 二、高耦合度代码1、服务端小改&#xff08;1&#xff09;维护一个unordered_map用户列表&#xff08;2&#xff09;服务端代码&#xff08;3&#xff09;客户端不改的情况下结果展示 2、大改客户端&#xff08;udp全双工用多…

通过QT进行服务器和客户端之间的网络通信

客户端 client.pro #------------------------------------------------- # # Project created by QtCreator 2024-07-02T14:11:20 # #-------------------------------------------------QT core gui network #网络通信greaterThan(QT_MAJOR_VERSION, 4): QT widg…

饥荒dst联机服务器搭建基于Ubuntu

目录 一、服务器配置选择 二、项目 1、下载到服务器 2、解压 3、环境 4、启动面板 一、服务器配置选择 首先服务器配置需要2核心4G&#xff0c;4G内存森林加洞穴大概就占75% 之后进行服务器端口的开放&#xff1a; tcp:8082 tcp:8080 UDP:10888 UDP:10998 UDP:10999 共…

套接字编程一(简单的UDP网络程序)

文章目录 一、 理解源IP地址和目的IP地址二、 认识端口号1. 理解 "端口号" 和 "进程ID"2. 理解源端口号和目的端口号 三、 认识协议1. 认识TCP协议2. 认识UDP协议 四、 网络字节序五、 socket编程接口1. socket 常见API2. sockaddr结构&#xff08;1&#…

输入设备应用编程-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

输入设备应用编程 输入类设备编程介绍 什么是输入设备 输入设备&#xff08;input 设备&#xff09;&#xff0c;如鼠标、键盘、触摸屏等&#xff0c;允许用户与系统交互 input 子系统 Linux系统通过input子系统管理多种输入设备 Input子系统提供统一的框架和接口&#xff…

网络编程之LINUX信号

注意发送信号是给进程&#xff0c;不是线程&#xff0c;调用的是KILL函数&#xff0c;SIG是信号种类。pid0是本进程的其他的进程。 可以通过设置ERRNO来查看返回的错误&#xff0c;如下&#xff1a; 当目标进程收到信号后&#xff0c;要对信号进行一些执行操作&#xff1a; 定义…

[每周一更]-(第106期):DNS和SSL协作模式

文章目录 什么是DNS&#xff1f;DNS解析过程DNS解析的底层逻辑 什么是SSL&#xff1f;SSL证书SSL握手过程SSL的底层逻辑 DNS与SSL的协同工作过程 什么是DNS&#xff1f; DNS&#xff08;Domain Name System&#xff0c;域名系统&#xff09;是互联网的重要组成部分&#xff0c…

黑马程序员MySQL基础学习,精细点复习【持续更新】

文章目录 数据库Mysql基础一、数据库1.数据库2.数据库管理系统3.SQL4.Mysql目录结构5.关系型数据库6.SQL基础概念 mysql高级一、数据库备份和还原1.图形化界面备份与还原 二、约束1.分类&#xff1a;2.主键约束3.唯一约束4.非空约束5.默认值约束6.外键约束 三、表关系1.概述2.一…