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文件…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案
在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
MLP实战二:MLP 实现图像数字多分类
任务 实战(二):MLP 实现图像多分类 基于 mnist 数据集,建立 mlp 模型,实现 0-9 数字的十分类 task: 1、实现 mnist 数据载入,可视化图形数字; 2、完成数据预处理:图像数据维度转换与…...
iOS 项目怎么构建稳定性保障机制?一次系统性防错经验分享(含 KeyMob 工具应用)
崩溃、内存飙升、后台任务未释放、页面卡顿、日志丢失——稳定性问题,不一定会立刻崩,但一旦积累,就是“上线后救不回来的代价”。 稳定性保障不是某个工具的功能,而是一套贯穿开发、测试、上线全流程的“观测分析防范”机制。 …...
