本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
系列文章目录
JAVA数据结构 DAY9 equals、Comparable、Comparator 与 PriorityQueue 深度解析
拓展目录
手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解
Java 中的 hashCode () 与 equals () 核心原理、契约规范、重写实践与面试全解
目录
目录
四、手动模拟实现优先级队列(MyPriorityQueue)
五、Java 官方 PriorityQueue 完整使用指南
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在数据结构与算法体系中,优先级队列是解决 “按优先级而非先后顺序出队” 场景的核心结构,而它的底层基石就是堆。无论是面试高频考点、算法题(Top-K、堆排序),还是工程中的任务调度、事件优先级处理,堆与优先级队列都是必须吃透的核心知识点。
本文将完整覆盖文档全部内容,从概念到原理、从手动实现到 Java API、从习题到应用,用最细致的讲解帮你彻底掌握。
一、优先级队列:打破普通队列 FIFO 规则的高级队列
1.1 普通队列的局限
我们之前学过的普通队列(Queue) 遵循严格的先进先出(FIFO) 规则:先入队的元素,一定先出队。
但现实中大量场景不适合 FIFO:
- 手机正在玩游戏,突然来电 → 来电必须优先处理
- 班级排座位 → 成绩更优的学生优先选座
- 操作系统任务调度 → 高优先级任务先执行
- 医院急诊 → 危重病人优先就诊
这些场景的核心需求:出队时,优先级最高的元素先出,普通队列无法满足。
1.2 优先级队列的定义
优先级队列(Priority Queue) 是一种特殊的队列,它不遵守先进先出,而是:
- 每次出队,都取出当前优先级最高的元素
- 每次入队,都能维持内部的优先级规则
它只需要提供两个最核心操作:
- 添加新元素
- 获取 / 删除最高优先级元素
在 JDK 1.8 中,Java 集合框架的 PriorityQueue 底层完全基于堆(Heap)实现,堆是优先级队列的底层数据结构。
二、堆(Heap):完全二叉树的特殊顺序存储结构
2.1 堆的官方定义
假设有一个关键码集合:K = {k0, k1, k2, …, kn-1}
把它按完全二叉树的层序规则存储在一维数组中,并且满足:
- 小堆:
Ki ≤ K[2i+1] 且 Ki ≤ K[2i+2] - 大堆:
Ki ≥ K[2i+1] 且 Ki ≥ K[2i+2]
满足以上条件的结构,就称为堆。
2.2 堆的两种形态
- 大根堆(最大堆):堆顶元素是整个堆的最大值,任意父节点 ≥ 子节点
- 小根堆(最小堆):堆顶元素是整个堆的最小值,任意父节点 ≤ 子节点
2.3 堆的两大核心性质
- 堆一定是一棵完全二叉树只有完全二叉树,才能用数组高效存储,不会浪费大量空间。
- 堆中任意节点的值,永远不大于 / 不小于它的孩子节点这是堆的 “有序性”,也是优先级队列能快速取最值的原因。
2.4 堆的顺序存储规则(下标公式)
堆用数组存储,通过下标可以O (1) 找到父节点、左孩子、右孩子:设当前节点下标为 i:
- 父节点下标:
(i - 1) / 2 - 左孩子下标:
2 * i + 1 - 右孩子下标:
2 * i + 2
注意:非完全二叉树不适合顺序存储,因为要存大量空节点,空间利用率极低。
三、堆的核心操作:向下调整、向上调整、建堆、插入、删除
堆的所有功能,都基于两个最基础的算法:向下调整、向上调整。
3.1 堆的向下调整(以小堆为例)
适用前提
以某节点为根的左子树、右子树已经是堆,只有根节点不满足堆性质,需要向下调整。
调整步骤(小堆)
- 用
parent标记需要调整的节点,child先标记左孩子(完全二叉树一定先有左孩子) - 如果右孩子存在,找出左右孩子中更小的那个,用
child标记它 - 比较
parent和child:- 如果
parent ≤ child:已经满足堆性质,结束调整 - 如果
parent > child:交换两者,继续向下调整
- 如果
- 循环直到
child超出数组范围(到叶子节点)
完整代码实现
/**
* 小堆的向下调整
* @param array 存储堆的数组
* @param parent 要调整的父节点下标
*/
public void shiftDown(int[] array, int parent) {
// 先指向左孩子
int child = 2 * parent + 1;
int size = array.length;
// 孩子存在才循环
while (child < size) {
// 右孩子存在,且更小,child 切换到右孩子
if (child + 1 < size && array[child + 1] < array[child]) {
child = child + 1;
}
// 父节点更小,满足堆,直接退出
if (array[parent] <= array[child]) {
break;
}
// 不满足,交换父节点与较小孩子
int temp = array[parent];
array[parent] = array[child];
array[child] = temp;
// 继续向下调整
parent = child;
child = 2 * parent + 1;
}
}
时间复杂度
最坏情况从根走到叶子,次数 = 完全二叉树高度时间复杂度:O (log₂n)
3.2 堆的创建(任意数组转堆)
如果数组是完全无序的(左右子树都不是堆),不能直接调整根节点,必须从底部往上批量调整。
建堆步骤
- 找到倒数第一个非叶子节点下标公式:
(array.length - 2) / 2或(array.length - 2) >> 1 - 从这个节点开始,向前遍历到根节点(下标 0)
- 每个节点都执行一次向下调整
建堆代码
/**
* 将普通数组调整为堆
*/
public static void createHeap(int[] array) {
// 找到倒数第一个非叶子节点
int lastParent = (array.length - 2) >> 1;
// 从后往前,逐个向下调整
for (int root = lastParent; root >= 0; root--) {
shiftDown(array, root);
}
}
3.3 建堆时间复杂度详细推导(面试常问)
我们用满二叉树近似计算(满二叉树是特殊的完全二叉树):设树高度为 h,总结点数 n ≈ 2ʰ - 1
总调整步数:T(n) = 2⁰·(h-1) + 2¹·(h-2) + 2²·(h-3) + … + 2ʰ⁻²·1
使用错位相减法:2·T(n) = 2¹·(h-1) + 2²·(h-2) + … + 2ʰ⁻¹·1
两式相减后化简:T(n) = 2ʰ - 1 - h ≈ n
最终结论:建堆的时间复杂度:O (N)
3.4 堆的插入操作
堆的插入必须保证插入后依然是堆。
插入步骤
- 把新元素放到数组末尾(完全二叉树最后一个位置)
- 对这个新元素执行向上调整,直到满足堆性质
向上调整代码(小堆)
/**
* 小堆向上调整
* @param child 新插入节点的下标
*/
public void shiftUp(int child) {
// 找到父节点
int parent = (child - 1) / 2;
while (child > 0) {
// 父节点更小,满足堆,结束
if (array[parent] <= array[child]) {
break;
}
// 交换
int temp = array[parent];
array[parent] = array[child];
array[child] = temp;
// 继续向上
child = parent;
parent = (child - 1) / 2;
}
}
3.5 堆的删除操作
堆的删除规则:只能删除堆顶元素(优先级最高 / 最低元素)
删除步骤
- 把堆顶元素和堆最后一个元素交换
- 堆的有效元素个数 减 1(逻辑删除)
- 对新的堆顶执行向下调整,恢复堆结构
四、手动模拟实现优先级队列(MyPriorityQueue)
结合上面的插入、删除、获取堆顶,我们可以写出一个最简可用的优先级队列(不考虑复杂扩容):
public class MyPriorityQueue {
// 底层数组存储堆
private int[] array = new int[100];
// 有效元素个数
private int size = 0;
// 入队:插入元素
public void offer(int e) {
array[size++] = e;
// 插入后向上调整
shiftUp(size - 1);
}
// 出队:删除堆顶并返回
public int poll() {
// 保存旧堆顶
int oldTop = array[0];
// 最后一个元素放到堆顶
array[0] = array[--size];
// 向下调整恢复堆
shiftDown(0);
return oldTop;
}
// 查看堆顶元素(不删除)
public int peek() {
return array[0];
}
// 向上调整
private void shiftUp(int child) {
// 上文代码
}
// 向下调整
private void shiftDown(int parent) {
// 上文代码
}
}
五、Java 官方 PriorityQueue 完整使用指南
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的。
Java 内置了开箱即用的优先级队列:java.util.PriorityQueue。
5.1 PriorityQueue 核心特性(必背)
关于PriorityQueue的使用要注意:
1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出
ClassCastException异常
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
5. 插入和删除元素的时间复杂度为
6. PriorityQueue底层使用了堆数据结构
7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
5.2 常用构造方法
| 构造方法 | 说明 |
|---|---|
PriorityQueue() | 创建空优先级队列,默认容量 11 |
PriorityQueue(int initialCapacity) | 指定初始容量(不能小于 1) |
PriorityQueue(Collection<? extends E> c) | 使用集合直接创建堆 |
示例:
public static void testConstruct() {
// 默认容量11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 指定容量100
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
// 用集合创建
List<Integer> list = Arrays.asList(4,3,2,1);
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
}
5.3 常用核心 API
| 方法 | 功能 |
|---|---|
boolean offer(E e) | 插入元素,失败抛异常 |
E peek() | 获取堆顶,空返回 null |
E poll() | 删除堆顶并返回,空返回 null |
int size() | 返回有效元素个数 |
void clear() | 清空队列 |
boolean isEmpty() | 判断是否为空 |
示例:
public static void testAPI() {
int[] arr = {4,1,9,2,8,0,7,3,6,5};
PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
// 入队
for (int num : arr) {
q.offer(num);
}
System.out.println(q.size()); // 10
System.out.println(q.peek()); // 0(小堆堆顶)
// 出队两次
q.poll();
q.poll();
System.out.println(q.peek()); // 2
q.offer(0);
System.out.println(q.peek()); // 0
q.clear();
System.out.println(q.isEmpty());// true
}
5.4 如何创建大根堆(面试高频)
默认是小根堆,想变成大根堆,必须传入自定义比较器(Comparator)。
// 自定义比较器:实现大根堆
class IntDescComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
// 降序:o2 - o1
return o2.compareTo(o1);
}
}
public class TestBigHeap {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>(new IntDescComparator());
pq.offer(4);
pq.offer(3);
pq.offer(1);
pq.offer(5);
// 输出 5(大堆堆顶)
System.out.println(pq.peek());
}
}
5.5 JDK 1.8 扩容机制(源码级)
PriorityQueue 自动扩容规则:
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 容量 <64:2倍扩容;≥64:1.5倍扩容
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// 超过最大限制时处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
简化总结:
- 容量 < 64 → 2 倍扩容
- 容量 ≥ 64 → 1.5 倍扩容
- 超过
Integer.MAX_VALUE - 8→ 按最大容量处理
六、堆的三大经典应用
6.1 堆排序
利用堆实现高效排序,时间复杂度 O (nlogn),空间复杂度 O (1)。
规则:
- 排升序 → 建大堆
- 排降序 → 建小堆
步骤:
- 把数组建成堆
- 循环:交换堆顶与最后一个元素 → 堆大小 - 1 → 向下调整堆顶
public void heapSort(){
int end=usedSize-1;
while(end>0){
swap(elem,0,end);
shiftDown(0,end);//范围截止到倒数第二个元素
end--;
}
}
public void swap(int[] elem,int i,int j){
int tmp=elem[i];
elem[i]=elem[j];
elem[j]=tmp;
}
//向下调整的时间复杂度 O(n)
private void shiftDown(int parent, int usedSize) {
int child=parent*2+1;//右子树
while(child<usedSize){
int tmp=elem[parent];
//找到左右子树的最大值
if(child+1<usedSize&&elem[child]<elem[child+1]){
child++;//左子树
}
if(tmp<elem[child]){
elem[parent]=elem[child];
elem[child]=tmp;
parent=child;
child=parent*2+1;
}else{
break;
}
}
}
6.2 Top-K 问题(面试 / 笔试最高频)
最大K个数
问题描述:在海量数据中,找出前 K 个最大 / 最小的数。比如:世界 500 强、成绩前 10 名、游戏战力前 100。
最优解法:堆(数据太大无法全部加载到内存时,只有堆能高效解决)
核心思路:
- 求前 K 个最大元素 → 建小根堆
- 求前 K 个最小元素 → 建大根堆
简单版代码(最小 K 个数):
class Solution {
public int[] smallestK(int[] arr, int k) {
if (arr == null || k <= 0) {
return new int[0];
}
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int num : arr) {
pq.offer(num);
}
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = pq.poll();
}
return res;
}
}

题目链接:
https://leetcode.cn/problems/xx4gT2/description/
class Solution {
public int findKthLargest(int[] nums, int k) {
int usedSize=nums.length;
if(k<0||k>usedSize){
System.out.println("K越界");
return -1;
}
// 1. 把数组【前k个元素】创建成 小根堆
createSmallHeap(nums,k);
// 2. 遍历【从k开始到末尾】的所有元素
for(int i=k;i<usedSize;i++){
// 3. 当前元素 > 堆顶(说明能进前k)
if(nums[0]<nums[i]){
nums[0]=nums[i];
shiftDownSmall(nums,0,k);
}
// 比堆顶小 → 直接跳过,不处理
}
return nums[0];
// 执行到这里:elem[0] ~ elem[k-1] 就是 前k个最大元素
}
// ===================== 工具方法 =====================
// 创建【大小为k】的小根堆
private void createSmallHeap(int[] nums,int k) {
for(int i=(k-1-1)/2;i>=0;i--){
shiftDownSmall(nums,i,k);
}
}
// 小根堆 向下调整(K-Top专用)
private void shiftDownSmall(int[] nums,int parent, int k) {
int child=parent*2+1;
while(child<k){
// 小根堆:找到 左右孩子中【更小】的那个
if(child+1<k&&nums[child]>nums[child+1]){
child++;
}
// 父节点 > 孩子 → 交换(小的往上浮)
if(nums[parent]>nums[child]){
swap(nums,parent,child);
parent=child;
child=parent*2+1;
}else {
break;
}
}
}
public void swap(int[] nums,int i,int j){
int tmp=nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
}
最小K个数
三个方法:
1.整体排序
2.整体建立一个大小为N的小根堆
3.把前K个元素创建为大根堆 遍历剩下的N-K个元素和堆顶元素比较,如果比堆顶元素小,则堆顶元素删除,当前元素入堆

题目链接:https://leetcode.cn/problems/smallest-k-lcci/description/
法2.整体建立一个大小为N的小根堆
class Solution {
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
priorityQueue.offer(arr[i]);
}
int[] ret = new int[k];
for(int i=0;i<k;i++){
ret[i]=priorityQueue.poll();
}
return ret;
}
}
法3.把前K个元素创建为大根堆 遍历剩下的N-K个元素和堆顶元素比较,如果比堆顶元素小,则堆顶元素删除,当前元素入堆
class Solution {
class IntCmp implements Comparator<Integer>{
//IntCmp:自定义比较器的名字(随便起)
//implements:实现一个接口
//Comparator<Integer>:比较器接口,专门用来比较 Integer 数字
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
//先讲 compareTo 是什么? Integer 自带的方法
//a.compareTo(b)
//负数:a 更小
//0:相等
//正数:a 更大
//所以:
//o1.compareTo(o2) → 升序(小在前,大在后)小根堆(默认)
//o2.compareTo(o1) → 降序(大在前,小在后)大根堆
}
}
public int[] smallestK(int[] arr, int k) {
int[] ret=new int[k];
//把前K个元素创建为大根堆 遍历剩下的N-K个元素和堆顶元素比较,如果比堆顶元素小,则堆顶元素删除,当前元素入堆
if(arr==null||k==0){
return ret;
}
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(k,new IntCmp());
for(int i=0; i<k;i++){
priorityQueue.offer(arr[i]);
//前K个元素是大根堆
}
for(int i=k;i<arr.length;i++){
//遍历剩下的N-K个元素和堆顶元素比较,如果比堆顶元素小,则堆顶元素删除,当前元素入堆
int peekVal=priorityQueue.peek();
if(arr[i]<peekVal){
priorityQueue.poll();
priorityQueue.offer(arr[i]);
}
}
for(int i=0;i<k;i++){
ret[i]=priorityQueue.poll();
}
return ret;
}
}
七、配套习题 + 详细答案
习题 1
下列关键字序列为堆的是:( )
A: 100,60,70,50,32,65
B: 60,70,65,50,32,100
C: 65,100,70,32,50,60
D: 70,65,100,32,50,60
E: 32,50,100,70,65,60
F: 50,100,70,65,60,32
答案:A解析:A 满足大根堆规则:父节点均大于等于子节点。
习题 2
已知小根堆为 8,15,10,21,34,16,12,删除关键字 8 之后重建堆,关键字之间的比较次数是 ( )
A:1 B:2 C:3 D:4
答案:C
- 删除堆顶 → 交换首尾
- 从根向下调整
- 选孩子:1 次
- 父子比较:1 次
- 下一层父子比较:1 次总共 3 次!
15 10
12 10
12 16
习题 3
最小堆 [0,3,2,5,7,4,6,8],删除堆顶元素 0 之后,其结果是 ( )
A: [3,2,5,7,4,6,8]
B: [2,3,5,7,4,6,8]
C: [2,3,4,5,7,8,6]
D: [2,3,4,5,6,7,8]
答案:C
习题 4
一组记录排序码为 (5,11,7,2,3,17),利用堆排序建立的初始堆为 ( )
A: (11,5,7,2,3,17)
B: (11,5,7,2,17,3)
C: (17,11,7,2,3,5)
D: (17,11,7,5,3,2)
E: (17,7,11,3,5,2)
F: (17,7,11,3,2,5)
答案:C
八、全文总结(最强思维导图版)
- 优先级队列 = 按优先级出队,底层是堆
- 堆 = 完全二叉树 + 顺序存储 + 父节点与子节点有序
- 堆分两种:大根堆、小根堆
- 堆核心操作:
- 向下调整:O (logn)
- 向上调整:O (logn)
- 建堆:O (N)
- Java
PriorityQueue:- 默认小堆
- 不能存 null、元素必须可比较
- 扩容:<64→2 倍,≥64→1.5 倍
- 堆最经典应用:堆排序、Top-K 问题

总结
以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!

1919

被折叠的 条评论
为什么被折叠?



