PriorityQueue分析
概述
PriorityQueue,优先级队列,一种特殊的队列,作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C++的优先队列每次取最大元素)。元素大小的评判可以通过元素本身的自然顺序(Natural Ordering),也可通过构造时传入的比较器(如Java中的Comparator,C++的仿函数)。
Java中PriorityQueue通过二叉小顶堆实现,可用一棵完全二叉树表示。PriorityQueue实现Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(Complete Binary Tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。
给每个元素按照层序遍历的方式进行编号,父子节点的编号之间的关系:
leftNo = parentNo*2+1
rightNo = parentNo*2+2
parentNo = (nodeNo-1)/2
通过上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。
应用场景
常用于需要按优先级处理元素的场景:
- 任务调度:调度系统中的任务可能有不同的优先级。使用PriorityQueue可确保高优先级的任务先执行。JDK中的定时任务调度可能会使用类似的结构来管理不同的任务;
- 图算法:许多图算法(如Dijkstra)需要频繁选择当前最小(或最大)权重的边或节点。PriorityQueue适合存储这些节点,以快速获取最优解;
- 实时数据处理:在一些流式处理框架中,可能需要实时处理数据并保持数据的优先级。PriorityQueue可帮助维护这些数据的顺序;
- MQ:在某些消息中间件中,可能会根据消息的优先级来处理消息。使用PriorityQueue可实现这一点,确保高优先级的消息先被消费;
- 事件驱动架构:在事件驱动系统中,基于事件优先级,PriorityQueue可方便地管理和调度这些事件;
- 合并多个有序序列:在处理多个已排序的数据流时,可使用PriorityQueue来合并这些序列,以保持整体的排序。
源码
PriorityQueue的peek()和element操作是常数时间,add(),offer(),无参数的remove()以及poll()方法的时间复杂度都是log(N)
。
方法剖析
add()和offer()
都是向优先队列中插入元素,Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后者则会返回false。
新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整:
public boolean offer(E e) {if (e == null)throw new NullPointerException();modCount++;int i = size;if (i >= queue.length)grow(i + 1);siftUp(i, e);size = i + 1;return true;
}
扩容函数grow()类似于ArrayList里的grow(),再申请一个更大的数组,并将原数组的元素复制过去。siftUp(int k, E x)
方法用于插入元素x并维持堆的特性。
private void siftUp(int k, E x) {if (comparator != null)siftUpUsingComparator(k, x, queue, comparator);elsesiftUpComparable(k, x, queue);
}private static <T> void siftUpComparable(int k, T x, Object[] es) {Comparable<? super T> key = (Comparable<? super T>) x;while (k > 0) {int parent = (k - 1) >>> 1;Object e = es[parent];if (key.compareTo((T) e) >= 0)break;es[k] = e;k = parent;}es[k] = key;
}private static <T> void siftUpUsingComparator(int k, T x, Object[] es, Comparator<? super T> cmp) {while (k > 0) {int parent = (k - 1) >>> 1;Object e = es[parent];if (cmp.compare(x, (T) e) >= 0)break;es[k] = e;k = parent;}es[k] = x;
}
调整过程:从k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]
为止。比较可以是元素的自然顺序,也可以是依靠比较器的顺序。
element()和peek()
语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,唯一的区别是当方法失败时前者抛出异常,后者返回null。根据小顶堆的性质,堆顶那个元素就是全局最小的那个;由于堆用数组表示,根据下标关系,0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可。
代码:
public E peek() {return (E) queue[0];
}
remove()和poll()
语义完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。
public E poll() {final Object[] es;final E result;if ((result = (E) ((es = queue)[0])) != null) {modCount++;final int n;final E x = (E) es[(n = --size)];es[n] = null;if (n > 0) {final Comparator<? super E> cmp;if ((cmp = comparator) == null)siftDownComparable(0, x, es, n);elsesiftDownUsingComparator(0, x, es, n, cmp);}}return result;
}
首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,之后调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。siftDown(int k, E x)
方法从k指定的位置开始,将x逐层向下与当前点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止。
private void siftDown(int k, E x) {if (comparator != null)siftDownUsingComparator(k, x, queue, size, comparator);elsesiftDownComparable(k, x, queue, size);
}private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {// assert n > 0;Comparable<? super T> key = (Comparable<? super T>)x;int half = n >>> 1; // loop while a non-leafwhile (k < half) {int child = (k << 1) + 1; // assume left child is leastObject c = es[child];int right = child + 1;if (right < n && ((Comparable<? super T>) c).compareTo((T) es[right]) > 0)c = es[child = right];if (key.compareTo((T) c) <= 0)break;es[k] = c;k = child;}es[k] = key;
}private static <T> void siftDownUsingComparator(int k, T x, Object[] es, int n, Comparator<? super T> cmp) {// assert n > 0;int half = n >>> 1;while (k < half) {int child = (k << 1) + 1;Object c = es[child];int right = child + 1;if (right < n && cmp.compare((T) c, (T) es[right]) > 0)c = es[child = right];if (cmp.compare(x, (T) c) <= 0)break;es[k] = c;k = child;}es[k] = x;
}
remove(Object o)
用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),源自Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,remove(Object o)
可分为2种情况:
- 删除的是最后一个元素。直接删除即可,不需调整;
- 不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。???
public boolean remove(Object o) {// 通过遍历数组的方式找到第一个满足o.equals(queue[i])元素的下标int i = indexOf(o);if (i == -1)return false;else {removeAt(i);return true;}
}
private int indexOf(Object o) {if (o != null) {final Object[] es = queue;for (int i = 0, n = size; i < n; i++)if (o.equals(es[i]))return i;}return -1;
}
E removeAt(int i) {// assert i >= 0 && i < size;final Object[] es = queue;modCount++;int s = --size;if (s == i) // removed last elementes[i] = null;else {E moved = (E) es[s];es[s] = null;siftDown(i, moved);if (es[i] == moved) {siftUp(i, moved);if (es[i] != moved)return moved;}}return null;
}
堆排序
堆排序利用堆的特性来排序一个数组,通常包含以下几个步骤:
- 构建最大堆
将输入数据构建成一个最大堆,从最后一个非叶子节点开始,逐层向上调整堆,确保每个父节点都大于或等于子节点。 - 排序过程
- 将最大堆的根节点(最大值)与堆的最后一个元素交换,然后将堆的大小减一(实际上是忽略最后一个元素);
- 对新的根节点进行堆调整,重新建立最大堆;
- 重复上述过程,直到堆的大小为1。
示例代码:
public class HeapSort {public static void heapSort(int[] arr) {PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);for (int num : arr) {maxHeap.offer(num);}for (int i = 0; i < arr.length; i++) {arr[i] = maxHeap.poll();}}public static void main(String[] args) {int[] arr = {4, 10, 3, 5, 1};heapSort(arr);for (int num : arr) {System.out.print(num + " ");}}
}
拓展
JDK
JDK源码里基于PriorityQueue实现的数据结构主要有两个:
- PriorityBlockingQueue:带优先级的阻塞队列
- DelayQueue:延迟队列
PriorityBlockingQueue
在线程池里看到这个类,一个支持优先级排序的无界阻塞队列。PriorityBlockingQueue不会阻塞生产者,而只是在没有可消费的数据时阻塞消费者。因此使用时需要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,最终会耗尽所有可用的内存空间。
PriorityBlockingQueue的几个属性:
- queue:数组,用来存放队列元素;
- size:用来存放队列元素个数;
- lock:独占锁对象,用来控制某个时间只能有一个线程可以进行入队、出队操作
- notEmpty:条件(Condition)变量,用来实现take方法阻塞模式(跟其它阻塞队列相比,这里没有notFull条件变量,这是因为PriorityBlockingQueue是无界队列,其put方法是非阻塞的)。
- allocationspinLock:是个自旋锁,使用CAS操作来保证某个时间只有一个线程可以扩容队列,状态为0或1,0表示当前没有进行扩容,1表示当前正在扩容;
PriorityBlockingQueue内部是使用平衡二叉树堆实现的,所以直接遍历队列元素不保证元素有序。
每次出队都返回优先级最高或最低的元素,默认使用对象的compareTo方法提供比较规则,这意味着队列元素必须实现Comparable接口;如果需要自定义比较规则则可以通过构造函数自定义comparator。
DelayQueue
ScheduledExecutorService
ScheduledExecutorService是JDK提供的用于定时任务调度API,支持设置任务优先级:
private ScheduledExecutorService schdExctr = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {private ThreadFactory factory = Executors.defaultThreadFactory();@Overridepublic Thread newThread(Runnable r) {Thread t = factory.newThread(r);t.setPriority(Thread.MIN_PRIORITY);return t;}
});
RocketMQ消息优先级
RocketMQ通过消息优先级实现消息的先入先出。消息优先级由生产者在发送消息时设置,范围为0-4,数字越大优先级越高。
Broker在接收到消息后,会将消息存储在不同的队列中,每种优先级对应一个队列。Broker会按照优先级从高到低的顺序消费队列中的消息,实现高优先级消息的先消费。
消费者在消费消息时,也会按照优先级从高到低的顺序拉取队列中的消息进行消费,保证高优先级消息的先消费。
RocketMQ通过设置消息优先级和隔离优先级消息到不同队列来实现消息的优先级,从而达到高优先级消息的先入先出。
仅供参考的代码:
// 生产者发送高优先级消息
Message msg = new Message("TopicTest", "TagA", "KEY", "Hello".getBytes());
msg.setKeys("KEY1");
msg.setPriority(3); // 设置消息优先级为3
producer.send(msg);// 消费者设置消费消息的优先级
pullConsumer.setConsumeMessageAck(true);
pullConsumer.registerMessageListener(new PriorityMessageListener());public class PriorityMessageListener implements MessageListener {@Overridepublic Action consumeMessage(MessageExt ext) { switch (ext.getPriority()) {case 3: // 消费优先级为3的消息 break;}}
}
参考
相关文章:

PriorityQueue分析
概述 PriorityQueue,优先级队列,一种特殊的队列,作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C的优先队列每次取最大元素)。元素大小的评判可以通过元素本身的自然顺序…...

Hive数仓操作(六)
一、 Hive 分区表 Hive 的分区表通过在 HDFS 中以不同的目录存储不同的分区数据,来提高查询性能并减少数据扫描量。分区表可以根据特定的列(如 性别 列的男/女)将数据划分为多个部分,使得查询时只需要扫描相关的分区,…...

centos7安装配置python3环境
1、wget https://www.python.org/ftp/python/3.11.2/Python-3.11.2.tgz 2、安装python依赖环境 切换到root用户,然后执行下面命令: 3、安装gcc,用于后续安装Python时编译源码: yum install gcc -y 4、安装Python3相关依赖&#…...

用 LoRA 微调 Stable Diffusion:拆开炼丹炉,动手实现你的第一次 AI 绘画
总得拆开炼丹炉看看是什么样的。这篇文章将带你从代码层面一步步实现 AI 文本生成图像(Text-to-Image)中的 LoRA 微调过程,你将: 了解 Trigger Words(触发词)到底是什么,以及它们如何影响生成结…...

手机实时提取SIM卡打电话的信令声音-(题外、插播一条广告)
手机实时提取SIM卡打电话的信令声音-(题外、插播一条广告) 前言 在去年的差不多这个时候,我们做了一遍外置配件的选型,筛选过滤了一批USB蓝牙配件和type-c转usb的模块。详情可参考《外置配件的电商价格和下载链接的选型.docx》一文:蓝牙电话…...

Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
目录 进程状态 状态决定了什么 进程等待方式——队列 进程状态的表现 挂起状态 基于阻塞的挂起——阻塞挂起 swap分区 进程状态表示 Z僵尸状态 进程的优先级 什么是进程的优先级 为什么会有进程的优先级 进程饥饿 Linux的调度与切换 切换 调度 queue [ 140 ]&am…...

Golang | Leetcode Golang题解之第456题132模式
题目: 题解: func find132pattern(nums []int) bool {candidateI, candidateJ : []int{-nums[0]}, []int{-nums[0]}for _, v : range nums[1:] {idxI : sort.SearchInts(candidateI, 1-v)idxJ : sort.SearchInts(candidateJ, -v)if idxI < idxJ {ret…...

回归预测|基于哈里斯鹰优化最小二乘支持向量机的数据回归预测Matlab程序HHO-LSSVM 多特征输入单输出含基础程序
回归预测|基于哈里斯鹰优化最小二乘支持向量机的数据回归预测Matlab程序HHO-LSSVM 多特征输入单输出含基础程序 文章目录 一、基本原理一、基本原理二、HHO-LSSVM的流程三、优缺点四、应用场景 二、实验结果三、核心代码四、代码获取五、总结 一、基本原理 HHO-LSSVM回归预测结…...

【Android 源码分析】Activity生命周期之onStop-1
忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。 – 服装…...

【Unity】本地化实现
个人向笔记。 1 前言 记录一下自己的本地化实现思路,暂时只讲本文的本地化实现。 2 文本本地化方案-个人 本地化实现是基于Luban的。自己使用Luban实现了一个“配置表模块”,又实现了一个“全局配置模块”,之后再基于这两个模块实现了“文本…...

Django一分钟:在Django中怎么存储树形结构的数据,DRF校验递归嵌套模型的替代方案
引言 在开发过程中我们可能需要这样的树形结构: [{"data": {"name": "牛奶"},"children": [{"data": {"name": "蒙牛"}, },{"data": {"name": "伊利"}, }]},{"da…...

【Docker从入门到进阶】06.常见问题与解决方案 07.总结与资源
6. 常见问题与解决方案 在使用Docker进行开发和部署过程中,可能会遇到各种问题。以下是一些常见问题及其解决方案: 容器启动失败和调试 在使用 Docker 时,容器启动失败或立即退出可能会导致一定的困扰,以下是进一步深入解决该问…...

快速排序的非递归实现:借助栈实现、借助队列实现
目录 用栈实现快速排序 1.用栈实现非递归快速排序的思路步骤 1.1.思路步骤 2.用栈实现非递归快速排序的代码 3.用栈实现非递归快速排序的整个工程 3.1.QuickSortNonR.h 3.2.QuickSortNonR.c 3.3.Stack.h 3.4.Stack.c 用队列实现非递归快速排序 1.用队列实现非递归快…...

Finops成本优化企业实践-可视化篇
引言:上一章讨论了finops的一些方法论,笔者在拿到finops官方认证finops-engineer certificate之后,将方法论运用到所在项目组中,并于今年完成了40%的费用节省。在此将这些实践方法总结沉淀,与大家分享。实践包括三篇&a…...

Spring Boot中线程池使用
说明:在一些场景,如导入数据,批量插入数据库,使用常规方法,需要等待较长时间,而使用线程池可以提高效率。本文介绍如何在Spring Boot中使用线程池来批量插入数据。 搭建环境 首先,创建一个Spr…...

Python机器学习:自然语言处理、计算机视觉与强化学习
📘 Python机器学习:自然语言处理、计算机视觉与强化学习 目录 ✨ 自然语言处理(NLP) 文本预处理:分词、去停用词词向量与文本分类:使用Word2Vec与BERT 🌆 计算机视觉基础 图像预处理与增强目标…...

Vue2 + ElementUI + axios + VueRouter入门
之前没有pc端开发基础,工作需要使用若依框架进行了一年的前端开发.最近看到一个视频框架一步步集成,感觉颇受启发,在此记录一下学习心得。视频链接:vue2element ui 快速入门 环境搭建和依赖安装 安装nodejs安装Vue Cli使用vue create proje…...

GO网络编程(四):海量用户通信系统2:登录功能核心【重难点】
目录 一、C/S详细通信流程图二、消息类型定义与json标签1. 消息类型定义2. JSON标签3.结构体示例及其 JSON 表示:4.完整代码与使用说明 三、客户端发送消息1. 连接到服务器2. 准备发送消息3. 创建 LoginMes 并序列化4. 将序列化后的数据嵌入消息结构5. 序列化整个 M…...

某项目实战分析代码二
某项目实战分析代码二 此次分析的是protobuf的使用操作流程具体实现 3. 业务数据分析3.1 客户端3.2 服务器端简单案例 此次分析的是protobuf的使用 Protocol Buffer( 简称 Protobuf) 是Google公司内部的混合语言数据标准,它是一种轻便高效的结构化数据存储格式&…...

全面指南:探索并实施解决Windows系统中“mfc140u.dll丢失”的解决方法
当你的电脑出现mfc140u.dll丢失的问题是什么情况呢?mfc140u.dll文件依赖了什么?mfc140u.dll丢失会导致电脑出现什么情况?今天这篇文章就和大家聊聊mfc140u.dll丢失的解决办法。希望能够有效的帮助你解决这问题。 哪些程序依赖mfc140u.dll文件…...

QT学习笔记1(QT和QT creator介绍)
QT学习笔记1(QT和QT creator介绍) Qt 是一个跨平台的应用开发框架,主要用于图形用户界面(GUI)应用的开发,但也支持非GUI程序的开发。Qt 支持多种平台,如Windows、macOS、Linux、iOS和Android&a…...

存储电话号码的数据类型,用 int 还是用 string?
在 Java 编程中,存储电话号码的选择可以通过两种常见方式进行:使用 int 类型或 String 类型。这种选择看似简单,但实际上涉及到 JVM 内部的字节码实现、内存优化、数据表示、以及潜在的可扩展性问题。 Java 基本数据类型与引用数据类型的差异…...

【目标检测】工程机械车辆数据集2690张4类VOC+YOLO格式
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):2694 标注数量(xml文件个数):2694 标注数量(txt文件个数):2694 标注…...

target_link_libraries()
target_link_libraries() 是 CMake 中的一个命令,用于指定目标(如可执行文件或库)所依赖的其他库。其主要作用包括: 链接库:将指定的库链接到目标上,使目标能够调用这些库中的函数和使用其功能。 管理依赖…...

Javascript数组研究09_Array.prototype[Symbol.unscopables]
Symbol.unscopables 是 JavaScript 中一个相对较新的符号(Symbol),用于控制对象属性在 with 语句中的可见性。它主要用于内置对象,如 Array.prototype,以防止某些方法被引入到 with 语句的作用域中,避免潜在…...

SkyWalking 自定义链路追踪
对项目中的业务方法,实现链路追踪,方便我们排查问题 引入依赖 <!‐‐ SkyWalking 工具类 ‐‐> <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm‐toolkit‐trace</artifactId> <vers…...

Linux驱动开发(速记版)--设备模型
第八十章 设备模型基本框架-kobject 和 kset 80.1 什么是设备模型 设备模型使Linux内核处理复杂设备更高效。 字符设备驱动适用于简单设备,但对于电源管理和热插拔,不够灵活。 设备模型允许开发人员以高级方式描述硬件及关系,提供API处理设备…...

动手学深度学习(李沐)PyTorch 第 6 章 卷积神经网络
李宏毅-卷积神经网络CNN 如果使用全连接层:第一层的weight就有3*10^7个 观察 1:检测模式不需要整张图像 很多重要的pattern只要看小范围即可 简化1:感受野 根据观察1 可以做第1个简化,卷积神经网络会设定一个区域,…...

新编英语语法教程
新编英语语法教程 1. 新编英语语法教程 (第 6 版) 学生用书1.1. 目录1.2. 电子课件 References A New English Grammar Coursebook 新编英语语法教程 (第 6 版) 学生用书新编英语语法教程 (第 6 版) 教师用书 1. 新编英语语法教程 (第 6 版) 学生用书 https://erp.sflep.cn/…...

Golang 服务器虚拟化应用案例
推荐学习文档 golang应用级os框架,欢迎stargolang应用级os框架使用案例,欢迎star案例:基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识,这里有免费的golang学习笔…...