Java数据结构:堆与PriorityQueue优先级队列的使用

news2024/11/24 11:03:50

文章目录

  • 1 什么是堆
  • 2 堆的实现思路
    • 2.1 大根堆的成员变量简介
    • 2.2 树的相关知识复习
    • 2.3 向下调整创建大根堆
    • 2.4 堆的插入
    • 2.5 堆的删除
  • 3 大根堆实现代码及测试
  • 4 PriorityQueue的使用
    • 4.1 特性简介
    • 4.2 常用方法
    • 4.3 使用PriorityQueue实现大根堆
  • 写在最后


1 什么是堆

 堆实质上就是对完全二叉树进行了一些调整。而在 Java 的 PriorityQueue 优先级队列中,底层就是堆这样的结构。所以,我们尝试通过模拟堆的实现,来更好的理解优先级队列。

知识补充:何为完全二叉树?
答:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

  对于一组关键码集合K = {k0, k1, k2, k3, …, kn-1}来说,如果对于所有的 k ,都满足 ki >= k2i+1 且 ki >= k2i+2,则说明该堆是个大根堆。反之,则称为小根堆。
 最直观的感受就是,对于大根堆来说,其根节点是最大的;对于,小根堆来说,其根节点是最小的。

我们以序列:{3, 7, 4, 2, 1, 8, 9, 10, 5} 为例,构造出大根堆如下图所示:
在这里插入图片描述
图中,红色序号为元素对应的数组下标,从左到右从上到下,从0开始依次编号。观察堆,可以总结出如下的特点:

  • 堆的某个节点的值总是不小于或者不大于父节点的值;
  • 堆总是一颗完全二叉树。

本文,将以大根堆为例,简述构造大根堆思路,并使用Java语言进行实现。


2 堆的实现思路

2.1 大根堆的成员变量简介

具体可见图中的代码注释,笔者对于大根堆的实现,都封装在BigHeap类中。
在这里插入图片描述

2.2 树的相关知识复习

 对于堆来说,其是对完全二叉树进行了些许调整,而如果我们使用顺序存储结构来表示堆,那么,对于双亲节点和孩子节点有如下的性质,在本文中,我们约定,所有的双亲节点采用parent指代,所有的孩子节点,采用child指代。而正如第一部分我们所叙述的一样,编号从上到下,从左到右依次从0开始编号。

  • 对于父亲节点 parent,其左孩子和有孩子的编号分别为2 * parent + 1, 2 * parent + 2;
  • 对于孩子节点child,其父亲节点满足 parent = (child - 1)/ 2;
  • 对于上述两点性质,均需要满足编号在 0 到 useSize-1 之间的边界范围。

2.3 向下调整创建大根堆

那么对于一个序列:{3, 7, 4, 2, 1, 8, 9, 10, 5},我们如何将其转化成为大根堆呢?

思路如下:

  1. 将该序列从左到右,从上到下,依次编号,构造完全二叉树后如图所示,所有蓝色圆圈中均为parent节点;
    在这里插入图片描述

  2. 最后一个 parent 节点的序号满足 (useSize-1-1)/ 2 ,我们从最后一个parent节点开始,依次向前调整,对于每个子树我们采用 向下调整 的策略;

  3. 从2所对应的紫色框的子树开始,判断parent与左右孩子的值之间的大小关系,将孩子节点中的较大值与parent节点进行比较,如果满足child > parent则进行交换,如图所示,此时,以2为根节点的子树已经调整为了大根堆;在这里插入图片描述

  4. 继续向前寻找parent节点,调整以4为根节点的子树,转化成大根堆,如图所示;
    在这里插入图片描述

  5. 调整以7为根节点的子树,将其向下调整为大根堆,7和10交换后,以7为根节点的新子树也需要调整为大根堆,但是由于其已经满足大根堆了,所以不作交换;
    在这里插入图片描述

  6. 最后调整以3为根节点的树,将其调整为大根堆,即将序列转化成了大根堆。
    在这里插入图片描述

相关代码如下:

    // 根据给定的数组向下调整构建大根堆 时间复杂度O(n)
    public void creadHeap(int[] data){
        // 依次向下调整初始数组的每一个元素 构建成堆
        // 从后往前找父节点
        this.elements = data;
        this.useSize = data.length;
        for (int parent = (useSize-1-1) / 2; parent >= 0; parent--) {
            siftDown(parent, useSize);
        }
    }

    // 向下调整
    // parent为要调整的父节点 len为每颗子树的调整范围
    private void siftDown(int parent, int len){
        int child = 2 * parent + 1; // 找到左孩子
        while (child < len){
            if (child + 1 < len && elements[child+1] > elements[child]){
                child = child + 1; // 保证child指向的位置一定是子节点中最大的 再与parent进行比较
            }
            if (elements[child] > elements[parent]){
                swap(elements, child, parent);
                parent = child; // 继续向下调整
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }

2.4 堆的插入

思路如下:

  • 先将元素放入堆的末尾,即最后一个孩子之后。从 elements 数组的角度来说,即将新插入的 val 放入到 elements[useSize] 的位置(需要注意是否需要扩容);
  • 将新插入的节点依次与双亲节点进行比较,向上调整, 直到与位置为 0 的 parent 比较后,调整完毕,此时,插入新节点后的新堆同样满足大根堆。

我们以插入一个新值val = 99来举例,示意图如下:
在这里插入图片描述
可以发现,每次只需要让新插入的节点和parent进行比较。

为什么不需要和兄弟节点再比较,选出较大值与parent节点进行交换呢?

因为,在每次插入新val前,已经有的元素均满足大根堆的性质,也就是说,对于每一个子树而言,其parent节点都是最大值,所以,就不需要和兄弟节点进行比较了。如果新插入的节点比parent大,其一定比兄弟节点大!

相关代码如下:

    // 判断堆是否为满
    public boolean isFull(){
        return useSize == elements.length;
    }

    // 入堆
    public void offer(int val){
        if (isFull()){
            elements = Arrays.copyOf(elements, elements.length + DEFAULT_CAPACITY); // 扩容
        }
        elements[useSize++] = val;
        // 向上调整
        siftUp(useSize-1);
    }

    // 向上调整 用于插入元素 每次插入元素放入数组最后的位置(注意容量) 然后向上调整重新构造
    private void siftUp(int child){
        // 依次与parent比较 若比parent大 则交换
        int parent = (child - 1) / 2;
        while (parent >= 0){
            if (elements[parent] < elements[child]){
                swap(elements, parent, child);
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }

2.5 堆的删除

而堆的删除,我们约定,删除的一定是 堆顶元素!

思路如下:

  • 将堆顶元素与最后一个元素进行交换;
  • 堆的有效数据减少一个,即 useSize = useSize - 1;
  • 对堆顶元素进行向下调整,使其结果满足大根堆。

我们以删除 val = 99 为例,示意图如下:

在这里插入图片描述
相关代码如下:

    // 判断是否为空
    public boolean isEmpty(){
        return useSize == 0;
    }

    // 删除堆顶元素 让堆顶元素和最后一个值替换 useSize-- 并且重新向下调整以堆顶元素起始的堆
    public int pop(){
        if (isEmpty()){
            throw new RuntimeException("堆为空");
        }
        int ret = elements[0];
        swap(elements, 0, useSize-1);
        useSize--;
        // 向下调整
        siftDown(0, useSize);
        return ret;
    }

3 大根堆实现代码及测试

在具体实现代码中,笔者额外实现了TopK问题,具体思路可以看代码注释,附上oj链接:最小k个数,读者可以自行练习。

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 大根堆
 */
@SuppressWarnings({"all"})
public class BigHeap {

    private int[] elements; // 存储堆的元素

    private int useSize; // 堆的大小

    private final int DEFAULT_CAPACITY = 10; // 默认容量

    public BigHeap(){
        this.elements = new int[DEFAULT_CAPACITY]; // 默认初始堆的大小为10
        this.useSize = 0;
    }

    // 根据给定的数组向下调整构建大根堆 时间复杂度O(n)
    public void creadHeap(int[] data){
        // 依次向下调整初始数组的每一个元素 构建成堆
        // 从后往前找父节点
        this.elements = data;
        this.useSize = data.length;
        for (int parent = (useSize-1-1) / 2; parent >= 0; parent--) {
            siftDown(parent, useSize);
        }
    }

    // 向下调整
    // parent为要调整的父节点 len为每颗子树的调整范围
    private void siftDown(int parent, int len){
        int child = 2 * parent + 1; // 找到左孩子
        while (child < len){
            if (child + 1 < len && elements[child+1] > elements[child]){
                child = child + 1; // 保证child指向的位置一定是子节点中最大的 再与parent进行比较
            }
            if (elements[child] > elements[parent]){
                swap(elements, child, parent);
                parent = child; // 继续向下调整
                child = 2 * parent + 1;
            }else {
                break;
            }
        }
    }

    // 判断堆是否为满
    public boolean isFull(){
        return useSize == elements.length;
    }

    // 入堆
    public void offer(int val){
        if (isFull()){
            elements = Arrays.copyOf(elements, elements.length + DEFAULT_CAPACITY); // 扩容
        }
        elements[useSize++] = val;
        // 向上调整
        siftUp(useSize-1);
    }

    // 向上调整 用于插入元素 每次插入元素放入数组最后的位置(注意容量) 然后向上调整重新构造
    private void siftUp(int child){
        // 依次与parent比较 若比parent大 则交换
        int parent = (child - 1) / 2;
        while (parent >= 0){
            if (elements[parent] < elements[child]){
                swap(elements, parent, child);
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }

    // 判断是否为空
    public boolean isEmpty(){
        return useSize == 0;
    }

    // 删除堆顶元素 让堆顶元素和最后一个值替换 useSize-- 并且重新向下调整以堆顶元素起始的堆
    public int pop(){
        if (isEmpty()){
            throw new RuntimeException("堆为空");
        }
        int ret = elements[0];
        swap(elements, 0, useSize-1);
        useSize--;
        // 向下调整
        siftDown(0, useSize);
        return ret;
    }

    // 交换arr的i j位置值
    private void swap(int[] arr, int i, int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 求前k个最大的值 则构造有k个元素的最小堆 每次入堆的时候和堆顶进行比较 若更大 则替换 最终剩下的就是前k个最大的值
    public int[] maxK(int[] arr, int k) {
        int[] ret = new int[k];
        // 合法性检验
        if(arr == null || k == 0) {
            return ret;
        }
        if(arr.length >= k){
            ret = Arrays.copyOf(arr, k);
            return ret;
        }
        Queue<Integer> minHeap = new PriorityQueue<>(k);
        //1、遍历数组的前K个 放到堆当中
        for(int i = 0; i < k; i++){
            minHeap.offer(arr[i]);
        }
        //2、遍历剩下的K-1个,每次和堆顶元素进行比较
        for (int i = k; i < arr.length; i++) {
            if (arr[i] > minHeap.peek()) {
                minHeap.poll(); // 出堆顶后添加
                minHeap.offer(arr[i]);
            }
        }
        //3、存储结果
        for (int i = 0; i < k; i++) {
            ret[i] = minHeap.poll();
        }
        return ret;
    }

    // 求前k个最小的值 构造大顶堆 如果新值比堆顶还要小 则替换 重新构造堆
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        // 合法性检验
        if (arr == null || k <= 0){
            return ret;
        }
        if (k >= arr.length){
            ret = Arrays.copyOf(arr, k);
        }
        // 构造大顶堆
        Queue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        // 先将前k个值入堆
        for (int i = 0; i < k; i++) {
            maxHeap.offer(arr[i]);
        }
        // 后判断剩余n-k个元素 如果比堆顶还要小 则替换
        for (int i = k; i < arr.length; i++) {
            if (arr[i] < maxHeap.peek()){
                maxHeap.poll();
                maxHeap.offer(arr[i]);
            }
        }
        // 存储并返回结果
        for (int i = 0; i < k; i++) {
            ret[i] = maxHeap.poll();
        }
        return ret;
    }

    // 以数组方式输出堆
    public void showHeap(){
        for (int i = 0; i < useSize; i++) {
            System.out.print(elements[i] + " ");
        }
        System.out.println();
    }
}

测试代码及测试结果如图:
在这里插入图片描述


4 PriorityQueue的使用

4.1 特性简介

 Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。 其继承体系如下:
在这里插入图片描述
几点说明:

  1. PriorityQueue中放置的元素 必须要能够比较大小, 不能插入无法比较大小的对象,否则会抛出ClassCastException异常
  2. 不能插入null对象, 否则会抛出NullPointerException
  3. 插入和删除元素的时间复杂度为O(logn)
  4. PriorityQueue底层使用了堆数据结构
  5. PriorityQueue默认情况下是小堆

4.2 常用方法

常用构造方法如下:
在这里插入图片描述
常用方法如下:
在这里插入图片描述
其余未列举的方法,读者可以自行参考帮助文档。

4.3 使用PriorityQueue实现大根堆

 刚刚提到,默认情况下 PriorityQueue 实现的是小根堆,那么如何实现大根堆呢?
 其实,只需要传入比较器,更改比较规则即可。在下面的代码样例中,笔者将一个存Integer对象的优先级队列,以大根堆的形式进行实现:

        // 构造大顶堆
        Queue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        
        // 构造大顶堆 lambda
        Queue<Integer> maxHeap2 = new PriorityQueue<>(((o1, o2) -> {
            return o2.compareTo(o1);
        }));

写在最后

本文被 Java数据结构 收录点击订阅专栏 , 持续更新中。
 创作不易,如果你有任何问题,欢迎私信,感谢您的支持!

在这里插入图片描述

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

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

相关文章

python对称加密AES的使用

python对称加密AES的使用 aes安装 pip install pycryptodome加密库引用 from Crypto.Util.Padding import pad, unpad from Crypto.Cipher import AES import base64完整代码 from Crypto.Util.Padding import pad, unpad from Crypto.Cipher import AES import base64def A…

带你玩转OpenHarmony AI-基于海思NNIE的AI能力自定义

简介相信大家从玩转OpenAtom OpenHarmony&#xff08;简称“OpenHarmony”&#xff09;AI系列专题的其他文章中&#xff0c;已经拓展了OpenHarmony AI在智慧出行、智慧办公等多场景下的AI自动检测、识别和判断的新体验&#xff0c;对于OpenHarmony平台上的AI开发有了一定认识。…

通过事件总线EventBus/AsyncEventBus进行JAVA模块解耦 (史上最全)

事件总线在 进行JAVA模块解耦 &#xff0c;价值巨大 实际开发中&#xff0c;常常 通过事件总线EventBus/AsyncEventBus进行JAVA模块解耦 &#xff0c; 比如&#xff0c;在顶级开源组件 hotkey的源码中&#xff0c; 就多次用到 EventBus/AsyncEventBus进行JAVA模块解耦 所以&am…

71、AdaNeRF: Adaptive Sampling for Real-time Rendering of Neural Radiance Fields

简介 官网&#xff1a;https://thomasneff.github.io/adanerf/ 新的双网络架构&#xff0c;它采用正交方向&#xff0c;通过学习如何最好地减少所需样本点的数量&#xff0c;将网络分为联合训练的 sample 和 shading 网络&#xff0c;训练方案在每条射线上采用固定的样本位置…

Nginx 高级篇

文章目录Nginx 高级篇一、 负载均衡1、 负载均衡概述2、 处理方式2.1 用户手动选择2.2 DNS 轮询2.3 四 / 七层负载均衡3、 七层负载均衡3.1 七层负载均衡指令3.1.1 upstream3.1.2 server3.2 实现流程3.3 负载均衡的状态3.3.1 down3.3.2 backup3.3.3 max_conns3.3.4 max_fails &…

Docker前世今生

文章目录Docker背景Docker历史docker 理念Docker能做什么虚拟机的缺点容器虚拟化技术Docker学习途径Docker背景 一款产品从开发到上线&#xff0c;从操作系统&#xff0c;到运行环境&#xff0c;再到应用配置。作为开发运维之间的协作我们需要 关心很多东西&#xff0c;这也是…

ChatGPTAPI Key申请教程

ChatGPTAPI Key申请教程 一、API Key申请使用 在浏览器打开网址&#xff1a;https://openai.com/api/ 等待网页加载完成后&#xff0c;点击右上角 LOG IN 进入登录界面 进入登录界面后&#xff0c;依次输入注册的邮箱–Continue–输入密码–Continue&#xff0c;完成登录&…

< CSS小技巧:filter滤镜妙用>

文章目录&#x1f449; 前言&#x1f449; 简述&#x1f449; 基本语法及案例》语法简述》案例&#x1f449; 拓展1. drop-shadow 更加智能的阴影效果2. 网页置灰3. 元素强调、高亮4.节省空间&#xff0c;提高网页加载速度&#x1f449; 具体案例网页参考文献往期内容 &#x1…

如何有效进行团队建设:从关注事到关注人

咱打工人都想趁着年终总结这个契机&#xff0c;拿着工作数据跟领导们提涨薪&#xff01;但是入行没多久的社畜们却没有这个底气&#xff0c;虽累但没结果&#xff08;暗指身兼数职的项目经理小白们&#xff09;&#xff0c;主要是觉得自己的工作成绩不够优秀。这几天办公室的项…

Model-Agnostic Meta-Learning for Fast Adaptation of Deep Networks

摘要 我们为元学习提出了一个算法是模型无关model−agnosticmodel-agnosticmodel−agnostic. 在某种意义上&#xff0c;其与用梯度下降训练的模型是兼容的&#xff0c;可以应用在大量不同的学习问题上。包括&#xff1a;分类、回归、和加强学习。 元学习的目标是正在学习任务…

TAZ生成实践(Intel芯片Mac Python 3.7.9)

参考文章 https://blog.csdn.net/weixin_42632778/article/details/115164518 TAZ生成 https://zhuanlan.zhihu.com/p/343576683 使用ArcGIS实现线转栅格 https://pro.arcgis.com/zh-cn/pro-app/latest/tool-reference/conversion/polyline-to-raster.htm ArcGIS Pro 折线转栅格…

第②篇 Spring IoC——容器

Spring最成功的是其提出的理念&#xff0c;而不是技术本身。 概念 Spring所依赖的两个核心理念&#xff1a; 一个是控制反转&#xff08;IoC&#xff09;。另一个是面向切面编程&#xff08;Aspect Oriented Programming&#xff0c;AOP&#xff09;。 IoC是Spring的核心&am…

JS入门到精通详解(1)

JavaScript概述(需要记)什么是javascript?是一门&#xff08;基于对象&#xff09;和&#xff08;事件驱动&#xff09;的&#xff08;脚本语言&#xff09;。js诞生于哪一年&#xff1f;哪个公司&#xff1f;谁&#xff1f;第一个名字叫什么&#xff1f;1995年 网景 布兰登 l…

【Python】type、isinstance、issubclass详解

type type方法有两种重载形式&#xff1a; type(o: object)&#xff1b;type(name: str, bases:Tuple[type, ...], dict:Mapping[str: Any], **kwds) 使用第一种重载形式的时候&#xff0c;传入一个【object】类型&#xff0c;返回一个【type】对象&#xff0c;通常与object…

解决使用element-plus时使用el-select-v2组件时,选中后无法移除focus的状态的方法。

我们可以使用element-ui-plus的el-select-v2的组件&#xff0c;实现复合搜索和下拉框的功能。 使用如下模块&#xff1a; <template><el-select-v2 v-model"value" filterable :options"options" placeholder"Please select"visibleCha…

爸妈记性变差怎么办?

记不住事的时候&#xff0c;我们总会自嘲“老了&#xff0c;脑子不好使了”。记忆力总是和年龄挂钩的&#xff0c;所以很多子女听到父母这样说&#xff0c;也不会放在心上。但有时&#xff0c;记性变差不一定因为年龄&#xff0c;还有可能是患病的前兆。当父母出现频繁忘事的情…

zerotier虚拟网络配置,局域网与外网如同局域网一样访问。

zerotier:可以搭建用于自己的虚拟网络&#xff0c;经过授权连接成功之后彼此都在同一网段&#xff0c;可以像在局域网一样互相访问。 1.创建zerotier账户 2.创建网络&#xff08;Create A Network&#xff09;并记住网络标识&#xff08;NETWORK ID&#xff09; 一、openwrt设…

中华财险进击数字化

本文来源 / 瞭望 中华联合财产保险股份有限公司&#xff08;下称中华财险&#xff09;&#xff0c;是一家 36 年的老牌国有保险公司&#xff0c;全国营业网点超过 2900 个。近年来&#xff0c;中华财险在业务高速发展的同时&#xff0c;从难啃的硬骨头下手&#xff0c;重构核心…

Unity 之 Addressable可寻址系统 -- 可寻址系统面板介绍 -- 入门(二)

可寻址系统面板介绍 -- 入门&#xff08;二&#xff09;一&#xff0c;可寻址系统目录介绍1.2 创建分组1.2 目录介绍二&#xff0c;可寻址系统设置介绍2.1 Profile - 配置文件2.2 Catalog - 目录2.3 Content Update - 内容更新2.4 Downloads - 下载2.5 Build - 构建2.6 Build a…

【数据结构】5.6 树和森林

文章目录5.6.1 树的存储结构&#xff08;不是二叉树&#xff09;双亲表示法孩子表示法结构定义双亲孩子法孩子兄弟法5.6.2 二叉树的转换树与二叉树的转换将树转换成二叉树将二叉树转换成树森林与二叉树的转换森林转换成二叉树二叉树转换成森林5.6.3 树和森林的遍历树的遍历森林…