前言
这篇文章来和大家分享一下优先级队列与PriorityQueue基本知识,内部逻辑,具体使用.
一、什么是优先级队列(堆)
优先级队列(Priority Queue) 是一种特殊的队列数据结构,它的核心特点是不再遵循"先进先出(FIFO)" 的原则,而是根据元素的优先级来决定出队顺序。
核心原理
- 入队规则
元素插入队列时,会根据自身的优先级被放置到合适的位置,保证队列始终是有序状态(按优先级升序或降序排列)。 - 出队规则
每次出队操作都会取出优先级最高的元素,而不是最早入队的元素。
常见实现方式
优先级队列通常基于以下两种数据结构实现:
- 堆(Heap)
这是最常用、最高效的实现方式,分为最大堆 和最小堆 :- 最大堆:根节点是队列中优先级最高的元素,出队时取出根节点。
- 最小堆:根节点是队列中优先级最低的元素,出队时取出根节点。
堆的插入和删除操作的时间复杂度均为 O ( log n ) O(\log n) O(logn),效率远高于普通数组或链表。
- 有序数组/链表
插入元素时直接按优先级排序,出队时直接取队首或队尾元素。
缺点是插入操作的时间复杂度为 O ( n ) O(n) O(n),数据量大时效率较低。
与普通队列的区别
| 特性 | 普通队列 | 优先级队列 |
|---|---|---|
| 排序规则 | 先进先出(FIFO) | 按元素优先级排序 |
| 出队对象 | 最早入队的元素 | 优先级最高的元素 |
| 常用实现 | 数组、链表 | 堆(最大堆/最小堆) |
在上面我们提到了优先级队列,一般是使用堆进行实现的,那什么是堆?又存在什么性质呢?
二、什么是堆?
堆(Heap) 是一种完全二叉树 结构的数组对象,同时满足堆序性质,是实现优先级队列的核心数据结构。
堆的两个核心条件
(1)结构条件:完全二叉树
完全二叉树的定义是:
- 除了最后一层,其他层的节点数都是满的;
- 最后一层的节点都靠左排列,没有空缺。
这种结构的优势是可以用数组直接存储,无需额外的指针,通过下标就能快速定位任意节点的父节点和子节点。
- 若节点下标为
i(数组下标从 0 开始):- 父节点下标:
(i - 1) // 2 - 左子节点下标:
2 * i + 1 - 右子节点下标:
2 * i + 2
- 父节点下标:
(2)堆序条件:父节点与子节点的大小关系
根据堆序的不同,堆分为两种:
- 最大堆(大顶堆)
每个父节点的值 ≥ 其左右子节点的值,堆顶(根节点)是整个堆的最大值。 - 最小堆(小顶堆)
每个父节点的值 ≤ 其左右子节点的值,堆顶(根节点)是整个堆的最小值。
注意:堆只要求父节点和子节点的大小关系,同一层的子节点之间没有顺序要求。
堆的核心操作
堆的操作围绕"维护堆序性质"展开,常见操作有:
-
插入(offer)
- 先将新元素添加到数组末尾(完全二叉树的最后一个位置);
- 从下往上调整(称为 上浮/Shift Up),将新元素与父节点比较,若不满足堆序则交换,直到满足条件。
- 时间复杂度: O ( log n ) O(\log n) O(logn)
-
删除堆顶(poll)
- 先取出堆顶元素(数组第一个元素);
- 将数组最后一个元素移到堆顶;
- 从上往下调整(称为 下沉/Shift Down),将堆顶元素与左右子节点比较,若不满足堆序则与更符合条件的子节点交换,直到满足条件。
- 时间复杂度: O ( log n ) O(\log n) O(logn)
三、常用代码手动实现

- 这一部分的逻辑是较为简单的,小伙伴们如果是第一次接触,非常建议大家上手实现一下~
我就都分成一个一个小的代码块了 大家在学习的时候也可以分成基本成员变量 ,成员方法,**辅助方法(在成员方法中被调用的小方法)**进行学习
基本方法
java
import java.nio.channels.Pipe;
import java.util.Arrays;
import java.util.concurrent.BlockingDeque;
public class MyHeap {
//成员变量
public int[] elem;
public int usedSize;
/****
*构造方法,容量实现
*/
public MyHeap(int cap) {
this.elem = new int[cap];
}
/***
* 将传入的数组实例化到elem数组之中,方便后面的调整
* @param arr
*/
public void init(int[] arr) {
for (int i = 0; i < arr.length; i++) {
elem[i] = arr[i];
usedSize++;
}
}
/***
* 建堆
* @param
*/
public void createHeap() {
for (int parent = (usedSize - 1 - 1) / 2; parent >= 0; parent--) {
siftDown(elem, parent);
}
}
public void siftDown(int[] arr, int p) {
//1.找到左右孩子的最大值
//1.1左孩子一定存在 判断右边的孩子 最终确定孩子的最大值
int child = 2 * p + 1;
if (child + 1 < usedSize && arr[child + 1] > arr[child]) {
child++;
}
//2.父亲与孩子最大值进行比较 进行交换 或者交换
while (p < usedSize) {
//孩子大 进行交换
if (arr[child] > arr[p]) {
swap(arr, p, child);
//更新p的值
p = child;
child = 2 * p + 1;
}else {
break;
}
}
}
public void siftUp(int[] arr, int child){
// 找到父节点
int parent = (child - 1) / 2;
// 当孩子节点不是根节点时循环
while (child > 0) {
// 如果孩子节点比父节点大,则交换
if (elem[child] > elem[parent]) {
swap(elem,child, parent);
// 继续向上调整
child = parent;
parent = (child - 1) / 2;
} else {
// 孩子节点已经比父节点小,满足大根堆性质
break;
}
}
}
public void swap(int[] arr ,int p,int c){
int tmp = arr[p];
arr[p] = arr[c];
arr[c] =tmp;
}
public void offer(int val){
//判断满
if (isFull()){
Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
siftUp(elem,usedSize);
usedSize++;
}
public int poll(){
//判断是不是空
if (isEmpty()){
return -1;
}
int top = elem[0];
elem[0] =elem[usedSize-1];
usedSize--;
//重新建堆
siftDown(elem,0);
return top;
}
public int peek(){
//判断是不是空
if (isEmpty()){
return -1;
}
int top = elem[0];
return top;
}
public int size(){
return usedSize-1;
}
public void clear(){
for (int i = 0; i < elem.length; i++) {
elem[i] = 0;
}
}
public boolean isFull(){
return usedSize == elem.length;
}
public boolean isEmpty(){
return elem.length == 0;
}
}
调整方式的选择
为什么在offer时候选择向上调整,在poll的时候选择向下调整?
这个问题问到了堆操作的核心逻辑------调整方向完全由新元素的初始位置和堆序规则决定,目的是用最少的比较交换次数,快速恢复堆的性质。
我们分别拆解 offer(插入)和 poll(删除堆顶)两个操作的逻辑:
1. offer(插入元素):选择向上调整(上浮)的原因
- 新元素的初始位置固定
堆是完全二叉树,插入元素时必须放在数组末尾 (也就是完全二叉树的最后一个空位),这是为了保证堆的结构条件不被破坏。 - 新元素的"目标位置"在上方
堆序规则要求父节点和子节点满足大小关系(比如最大堆中父 ≥ 子)。新元素在数组末尾,它只有父节点,没有子节点。- 如果新元素比父节点更"符合堆顶要求"(比如最大堆里新元素比父节点大),就需要和父节点交换,一步一步往上挪。
- 这个过程只需要和父节点单向比较 ,直到找到自己的正确位置,也就是向上调整。
- 反向思考:如果此时向下调整,新元素根本没有子节点,调整就无从谈起。
2. poll(删除堆顶):选择向下调整(下沉)的原因
- 堆顶空缺的填补方式
删除堆顶后,堆的结构被破坏,我们会把数组最后一个元素移到堆顶,填补空缺(这是保证完全二叉树结构的最优方式)。 - 新堆顶的"目标位置"在下方
移到堆顶的这个元素,原本在数组末尾,大概率不满足堆序规则。此时它有左右两个子节点 ,没有父节点。- 我们需要把它和更符合堆序的子节点交换(比如最大堆里,和更大的子节点交换),一步一步往下挪。
- 这个过程只需要和子节点单向比较 ,直到找到正确位置,也就是向下调整。
- 反向思考:如果此时向上调整,新堆顶已经是根节点,没有父节点,调整无法进行。
四、PriorityQueue
- 有了上面的手动是实现,我们对堆有了一个简单理解,在java官方中也有类似的结构已经封装好了,我们可以直接使用,这就是PriorityQueue.
在编程中,PriorityQueue 是优先级队列的实现类 ,不同编程语言的标准库中都有对应的实现,核心逻辑基于堆结构,遵循"优先级高的元素先出队"的规则。
Java 的 java.util.PriorityQueue 是基于最小堆实现的优先级队列,是最典型的实现之一。
1. 核心特性
- 默认规则 :默认按元素的自然顺序升序排列,堆顶是优先级最低(值最小)的元素。
- 自定义优先级 :可以通过传入
Comparator接口的实现类,自定义元素的比较规则(比如改成最大堆)。 - 底层结构 :基于动态数组实现的完全二叉树,支持自动扩容。
- 线程安全性 :非线程安全;如果需要线程安全的版本,可使用
PriorityBlockingQueue。
2. 常用方法
| 方法 | 功能 | 时间复杂度 |
|---|---|---|
add(E e) / offer(E e) |
插入元素,维护堆序 | O ( log n ) O(\log n) O(logn) |
peek() |
获取堆顶元素(不删除) | O ( 1 ) O(1) O(1) |
poll() |
移除并返回堆顶元素 | O ( log n ) O(\log n) O(logn) |
remove(Object o) |
移除指定元素 | O ( n ) O(n) O(n) |
isEmpty() |
判断队列是否为空 | O ( 1 ) O(1) O(1) |
size() |
获取队列元素个数 | O ( 1 ) O(1) O(1) |
3.Priority Queue 构造方法
Java 中的 PriorityQueue 位于 java.util 包下,其构造方法共有 7 个重载版本 ,核心是围绕 初始容量 和 比较器(自定义优先级规则) 来设计的。
所有构造方法的底层逻辑都是:初始化一个基于动态数组的最小堆(默认) ,如果传入了 Comparator,则按照比较器规则维护堆序。
核心前提
- 默认排序规则 :如果没有指定比较器,
PriorityQueue会要求元素实现Comparable接口,按照自然顺序升序排列(最小堆)。 - 初始容量的作用:底层数组的初始长度,当元素数量超过容量时,会自动扩容(扩容规则:当前容量 < 64 时,扩容为 2 倍 + 2;≥ 64 时,扩容为 1.5 倍)。
- 线程安全性 :所有构造方法创建的
PriorityQueue都是非线程安全 的,线程安全场景需使用PriorityBlockingQueue。
7 个构造方法详解
1. 无参构造方法
java
public PriorityQueue()
-
作用 :创建一个初始容量为 11 的
PriorityQueue,遵循元素的自然顺序(最小堆)。 -
要求 :存入的元素必须实现
Comparable接口(如Integer、String),否则会抛出ClassCastException。 -
示例 :
javaPriorityQueue<Integer> pq = new PriorityQueue<>(); pq.offer(5); pq.offer(2); System.out.println(pq.peek()); // 输出 2(自然升序,最小堆)
2. 指定初始容量的构造方法
java
public PriorityQueue(int initialCapacity)
-
参数 :
initialCapacity- 初始容量,必须 ≥ 1,否则抛出IllegalArgumentException。 -
作用 :创建一个指定初始容量的
PriorityQueue,遵循元素的自然顺序。 -
适用场景:提前知道元素数量,指定初始容量可以减少扩容次数,提升性能。
-
示例 :
java// 初始容量 20,避免频繁扩容 PriorityQueue<String> pq = new PriorityQueue<>(20); pq.offer("B"); pq.offer("A"); System.out.println(pq.peek()); // 输出 A(字符串自然升序)
3. 指定初始容量 + 比较器的构造方法
java
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
-
参数
initialCapacity:初始容量(≥ 1)。comparator:自定义的比较器,用于指定优先级规则;可以为null,此时等价于自然顺序。
-
作用 :创建指定容量 + 自定义优先级的
PriorityQueue,最常用的构造方法之一。 -
适用场景 :元素没有实现
Comparable,或者需要自定义优先级(比如最大堆)。 -
示例(构建最大堆) :
java// 初始容量 10,比较器为降序,构建最大堆 PriorityQueue<Integer> maxHeap = new PriorityQueue<>(10, Comparator.reverseOrder()); maxHeap.offer(3); maxHeap.offer(1); maxHeap.offer(5); System.out.println(maxHeap.peek()); // 输出 5(最大堆顶)
4. 传入集合(Collection)的构造方法
java
public PriorityQueue(Collection<? extends E> c)
-
参数 :
c- 要转换为优先级队列的集合(如ArrayList、HashSet)。 -
作用 :
- 如果集合
c是SortedSet或另一个PriorityQueue,则新队列继承原集合的排序规则。 - 否则,按照自然顺序构建最小堆。
- 如果集合
-
容量 :新队列的初始容量等于集合
c的大小。 -
示例 :
javaList<Integer> list = Arrays.asList(7, 2, 9); PriorityQueue<Integer> pq = new PriorityQueue<>(list); System.out.println(pq.peek()); // 输出 2(自然升序)
5. 传入优先级队列(PriorityQueue)的构造方法
java
public PriorityQueue(PriorityQueue<? extends E> c)
-
参数 :
c- 另一个PriorityQueue。 -
作用 :创建一个与原
PriorityQueue相同容量、相同比较器、相同元素的新队列。 -
注意 :是浅拷贝,元素本身不会被复制。
-
示例 :
javaPriorityQueue<Integer> src = new PriorityQueue<>(Comparator.reverseOrder()); src.offer(5); src.offer(1); // 新队列和原队列一样是最大堆 PriorityQueue<Integer> dest = new PriorityQueue<>(src); System.out.println(dest.peek()); // 输出 5
6. 传入有序集合(SortedSet)的构造方法
java
public PriorityQueue(SortedSet<? extends E> c)
-
参数 :
c- 一个SortedSet(如TreeSet)。 -
作用 :创建的新队列继承
SortedSet的比较器和元素 ,容量等于SortedSet的大小。 -
示例 :
java// TreeSet 默认自然升序 SortedSet<String> set = new TreeSet<>(Arrays.asList("C", "A", "B")); PriorityQueue<String> pq = new PriorityQueue<>(set); System.out.println(pq.peek()); // 输出 A
构造方法核心对比表
| 构造方法 | 初始容量 | 排序规则 | 适用场景 |
|---|---|---|---|
| 无参 | 11 | 自然顺序 | 快速创建最小堆,元素是 Comparable 类型 |
| 指定容量 | 自定义 | 自然顺序 | 已知元素数量,减少扩容 |
| 容量 + 比较器 | 自定义 | 自定义规则 | 自定义优先级(如最大堆) |
| 传入 Collection | 集合大小 | 自然顺序(SortedSet/PQ 除外) | 直接将集合转为优先级队列 |
| 传入 Comparator(Java8+) | 11 | 自定义规则 | 无需指定容量的自定义优先级场景 |
总结
- 到这里我的分享就先结束了~,希望对你有帮助
- 我是dylan 下次见~
- 无限进步