JDK源码剖析之PriorityQueue优先级队列

news2024/9/23 19:25:59

写在前面

版本信息:
JDK1.8

PriorityQueue介绍

在数据结构中,队列分为FIFOLIFO 两种模型,分别为先进先出,后进后出先进后出,后进先出(栈) 而一切数据结构都是基于数组或者是链表实现。

在Java中,定义了Queue接口,接口中定义了CRUD的基本方法。分别add、offer、remove、poll等等,而PriorityQueue 实现此接口实现了基本的CRUD的同时拥有了自己的特性,从名字来看也能知道是优先级队列 : 保持队列头部节点是整条队列中永远是最小或者最大的节点,其实现原理就是一个小顶堆或者大顶堆。上文提及到一切数据结构都是基于数组或者是链表实现,而这里使用了数组实现。

public class PriorityQueue<E> extends AbstractQueue<E>
    implements java.io.Serializable {
    transient Object[] queue;    // 由数组实现
}

public abstract class AbstractQueue<E>
    extends AbstractCollection<E>
    implements Queue<E> {}

小顶堆和大顶堆介绍

从上文描述了PriorityQueue的底层实现是小顶堆或者大顶堆,那么在看源码之前,我们需要先明白小顶堆和大顶堆如何实现~

小顶堆:一颗完全二叉树,其中任意父节点都要小于左右子节点,所以树的根节点是整棵树的最小节点

大顶堆:一颗完全二叉树,其中任意父节点都要大于左右子节点,所以树的根节点是整棵树的最大节点

Comparable和Comparator区别

在看PriorityQueue源码之前还需要分析Comparable和Comparator区别。

Comparable:类需要实现此接口,重写compareTo方法,在compareTo方法中定义比较逻辑,使用时把类强转成Comparable调用compareTo方法,把比较对象传入。所以侵入性比较强,与业务代码强耦合。

Comparator:这个就是一个比较器,只需要把A比较对象和B比较对象都传入即可,不需要于业务代码强耦合

PriorityQueue添加元素源码分析

下文直接把PriorityQueue叫成小顶堆

我们直接从offer方法入手~

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;        // 用于检测是否并发
    // 因为size从0开始,所以size的值就是数组的索引值。
    int i = size;

    // 是否需要扩容
    if (i >= queue.length)
        grow(i + 1);  

    // 为下次索引+1
    size = i + 1;

    // 如果是第一个,那么就直接占用数组第一个元素即可,
    // 因为不管是小顶堆还是大堆堆第一个都直接插入。
    if (i == 0)
        queue[0] = e;
    else
        // 非第一个节点,此时就需要调整
        siftUp(i, e);
    return true;
}

这里的逻辑比较简单,因为这里使用数组实现的小顶堆(也即使用数组实现完全二叉树),而小顶堆的第一个节点是最小的,所以当0索引直接插入即可,非0索引就需要调整小顶堆。这里应该有很多读者是第一次见数组实现二叉树,所以这里把上文的二叉树进行扩展,把数组部分画上去。

在看 siftUp调整方法前,我们看一下grow扩容方法, 因为里面有一个思路大家可以学习~

private void grow(int minCapacity) {
    int oldCapacity = queue.length;
    
    // 容量小于64时,扩容为 oldCapacity + oldCapacity +2 
    // 容量大于64,扩容为 oldCapacity + oldCapacity/2
    // 等同于,在容量小的时候,每次扩容大一些,当达到64这个阈值后,扩容小一些,要不然空间会太浪费了~
    int newCapacity = oldCapacity + ((oldCapacity < 64) ?
       (oldCapacity + 2) :
       (oldCapacity >> 1));

    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 数组拷贝迁移。
    queue = Arrays.copyOf(queue, newCapacity);      
}

在每次扩容的时候,会去判断,当前容量是否大于64,如果小于64就直接 原大小 * 2 + 2 扩容,如果大于64以后直接 原大小 + 原大小/2 扩容。目的是为了在容量小的时候扩容大一些,减少扩容次数。在容量达到64阈值后,扩容小一些,减少内存浪费。

下面开始讲解siftUp调整方法

private void siftUp(int k, E x) {
    // 用户是否传入comparator比较器
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        // 没传入就使用Comparable
        // 此时类需要实现Comparable接口
        siftUpComparable(k, x);
}

这里讲解siftUpComparable方法,本质上两个方法没任何区别~

private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        // 拿到父节点
        // 因为是使用数组实现的一颗完全二叉树,所以直接-1 右移即可拿到当前插入节点的父节点
        int parent = (k - 1) >>> 1;

        Object e = queue[parent];
        // 与父节点做比较。
        if (key.compareTo((E) e) >= 0)
            // 达到用户的预期比较就直接break,要不然继续往父节点的父节点继续做比较,直到根节点
            break;
        // 没达到预期,所以把父节点插入到本次插入的节点的位置。
        queue[k] = e;
        // 拿到父节点的索引,继续往父节点的父节点做比较。
        k = parent;
    }
    // 插入
    queue[k] = key;
}

这里光看注释,肯定是看不明白的,所以以画图+注释来理解吧~

PriorityQueue获取元素源码分析

直接从poll方法入手~

public E poll() {
    if (size == 0)
        return null;
    // 拿到最后一个节点的索引值。
    int s = --size;
    modCount++;
    // 因为第一个是小顶堆或者大顶堆要的数据。
    E result = (E) queue[0];
    // 拿到最后一节点
    E x = (E) queue[s];
    queue[s] = null;   // help gc
    if (s != 0)
        // 调整
        siftDown(0, x);
    return result;
}

因为小顶堆或者大顶堆都是拿第一个元素,所以这里拿出第一个元素。但是每次拿完就需要调整小顶堆(调整完全二叉树),所以看到siftDown方法。

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

本质上这两个方法没任何区别,所以继续看到siftDownComparable方法

// k为0
// x是最后一个节点。
private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;

    // 循环的次数
    // 是通过数组的大小 右移一位就可以知道树高了。
    int half = size >>> 1;        
    while (k < half) {
        // 往下层找。
        int child = (k << 1) + 1; 
        Object c = queue[child];    // 左子节点
        int right = child + 1;      // 右子节点

        // 左右子节点比较,那个满足规范就作为父节点
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            // 右节点满足于左节点
            c = queue[child = right];
        // 与最后一个节点比较后,达到预期直接退出
        if (key.compareTo((E) c) <= 0)
            break;
        // 替换
        queue[k] = c;
        // 下次循环的父节点
        k = child;
    }
    queue[k] = key;
}

因为每次poll取走的是第一个元素,所以需要调整整个小顶堆,而第一个元素是小顶堆的根节点,所以需要调整小顶堆找到一个符合的元素作为根节点。从根节点的左右子节点开始比较,左右子节点比较出预期的节点就作为新的根节点。预期的节点作为下次比较的父节点,通过父节点再找到他的左右子节点做比较,周而复始,直到最后一个节点。

这里光看注释,肯定是看不明白的,所以以画图+注释来理解吧~

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

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

相关文章

线上问诊:可视化展示

系列文章目录 线上问诊&#xff1a;业务数据采集 线上问诊&#xff1a;数仓数据同步 线上问诊&#xff1a;数仓开发(一) 线上问诊&#xff1a;数仓开发(二) 线上问诊&#xff1a;数仓开发(三) 线上问诊&#xff1a;可视化展示 文章目录 系列文章目录前言一、全流程调度1.生产新…

两两交换链表中节点

给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1,4…

【python 多线程】初体验+单线程下载器+并行下载器

1.多线程初体验 主线程的id和进程的id是一个 查看进程pid下有多少个线程 ps -T -p pid(base) D:\code\python_project\python_coroutine>C:/ProgramData/Anaconda3/python.exe d:/code/python_project/python_coroutine/01demo.py threading.active_count1 i am producer…

vue手写提示组件弹窗

1、弹框展示 2、message组件 新建一个message.vue <template><div class"wrapper" v-if"isShow" :class"showContent ? fadein : fadeout">{{ text }}</div> </template> <script></script> <style s…

智能小车之跟随小车、避障小车原理和代码

目录 1. 红外壁障模块分析​编辑 2. 跟随小车的原理 3. 跟随小车开发和调试代码 4. 超声波模块介绍 5. 摇头测距小车开发和调试代码 1. 红外壁障模块分析 原理和循迹是一样的&#xff0c;循迹红外观朝下&#xff0c;跟随朝前 TCRT5000传感器的红外发射二极管不断发射红外…

使用半导体材料制作霍尔元件的优点

霍尔元件是一种基于霍尔效应的传感器&#xff0c;可以测量磁场强度和电流等物理量。霍尔效应是指&#xff0c;当电流通过一块导体时&#xff0c;如果该导体置于垂直于电流方向的磁场中&#xff0c;就会在导体两侧出现一定的电势差&#xff0c;这就是霍尔效应。霍尔元件可以利用…

亚马逊云科技与百川智能发起AI黑客松,共探医疗健康和游戏娱乐领域的前沿应用

8月31日&#xff0c;亚马逊云科技云创计划成员企业暨基础模型创业公司百川智能&#xff0c;率先通过了《生成式人工智能服务管理暂行办法》备案&#xff0c;即日起面向全社会开放服务。基础模型获准面向公众用户开放服务&#xff0c;意味着有机会基于大量真实用户的调用反馈建立…

Android使用Kotlin封装MMKVUtils

Android使用Kotlin封装MMKVUtils 1.简介&#xff1a; MMKV 是基于 mmap 内存映射的 key-value 组件&#xff0c;底层序列化/反序列化使用 protobuf 实现&#xff0c;性能高&#xff0c;稳定性强。从 2015 年中至今在微信上使用&#xff0c;其性能和稳定性经过了时间的验证。近…

【第二章 数据的表示和运算】2.3

IEEE规格化&#xff1a; 18&#xff08;阶码移码偏置值127&#xff0c;取值范围1-254&#xff0c;负值要补码取原码&#xff09;23&#xff08;隐1.原码&#xff09;

测试阶段之冒烟测试

冒烟测试 一般建议1-2个小时完成冒烟测试。 注意冒烟用例不是P1P2&#xff0c;而是其中的部分用例

yum安装mysql5.7散记

## 数据源安装 $ yum -y install wget $ wget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm $ yum localinstall mysql57-community-release-el7-8.noarch.rpm $ yum repolist enabled | grep "mysql.*-community.*" $ yum install mysql-…

IDEA Properties 文件亂碼怎麼解決

1.FIle->Setting->Editor->File Encodings 修改Properties FIles 編碼顯示格式&#xff1a;UTF-8

一百七十二、Flume——Flume采集Kafka数据写入HDFS中(亲测有效、附截图)

一、目的 作为日志采集工具Flume&#xff0c;它在项目中最常见的就是采集Kafka中的数据然后写入HDFS或者HBase中&#xff0c;这里就是用flume采集Kafka的数据导入HDFS中 二、各工具版本 &#xff08;一&#xff09;Kafka kafka_2.13-3.0.0.tgz &#xff08;二&#xff09;…

Netty编程面试题

1.Netty 是什么&#xff1f; Netty是 一个异步事件驱动的网络应用程序框架&#xff0c;用于快速开发可维护的高性能协议服务器和客户端。Netty是基于nio的&#xff0c;它封装了jdk的nio&#xff0c;让我们使用起来更加方法灵活。 2.Netty 的特点是什么&#xff1f; 高并发&a…

React【组件生命周期 、组件生命周期_挂载、 组件生命周期_更新 、组件生命周期_卸载、表单_受控组件、表单_受控组件处理多个输入】(三)

文章目录 组件生命周期 组件生命周期_挂载 组件生命周期_更新 组件生命周期_卸载 表单_受控组件 表单_受控组件处理多个输入 组件生命周期 每个组件都有自己的生命周期&#xff0c;从“生”到”死“。 在这个过程当中&#xff0c;它会有不同的状态&#xff0c;针对不同的状态…

深入探究数据结构与算法:构建强大编程基础

文章目录 1. 为什么学习数据结构与算法&#xff1f;1.1 提高编程技能1.2 解决复杂问题1.3 面试准备1.4 提高代码效率 2. 学习资源2.1 经典教材2.2 在线学习平台2.3 学习编程社区 3. 数据结构与算法的实际应用3.1 排序算法3.2 图算法3.3 字符串匹配算法 4. 结论 &#x1f389;欢…

【前端】WebWorker 在前端SPA框架的应用

一、什么是WebWorker 概念&#xff1a; Web Worker是一种在Web浏览器中运行的JavaScript脚本&#xff0c;它可以在后台线程中运行&#xff0c;而不会阻塞主线程。这意味着Web Worker可以在后台执行复杂的计算任务&#xff0c;而不会影响用户界面的响应性能 除了标准的JavaScri…

C++之生成key-value键值三种方式(一百九十)

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

vue前后端端口不一致解决方案

在config index.js文件中 引入如下代码即可 const path require(path) const devEnv require(./dev.env) module.exports {dev: {// PathsassetsSubDirectory: static,assetsPublicPath: /,proxyTable: devEnv.OPEN_PROXY false ? {} : {/api: {target: http://localhos…

swiper删除虚拟slide问题

在存在缓存的情况下&#xff0c;删除较前的slide&#xff0c;会出现当前slide与后一个slide重复出现的情况 假设当前存在5个slide&#xff0c;且这5个slide已缓存&#xff0c;则删除slide2后&#xff0c;仍为5个slide&#xff0c;且slide2的内容变为slide3的内容&#xff0c;此…