本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
系列文章目录
拓展目录
手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解
目录
目录
[一、优先级队列:打破普通队列 FIFO 规则的高级队列](#一、优先级队列:打破普通队列 FIFO 规则的高级队列)
[1.1 普通队列的局限](#1.1 普通队列的局限)
[1.2 优先级队列的定义](#1.2 优先级队列的定义)
[2.1 堆的官方定义](#2.1 堆的官方定义)
[2.2 堆的两种形态](#2.2 堆的两种形态)
[2.3 堆的两大核心性质](#2.3 堆的两大核心性质)
[2.4 堆的顺序存储规则(下标公式)](#2.4 堆的顺序存储规则(下标公式))
[3.1 堆的向下调整(以小堆为例)](#3.1 堆的向下调整(以小堆为例))
[3.2 堆的创建(任意数组转堆)](#3.2 堆的创建(任意数组转堆))
[3.3 建堆时间复杂度详细推导(面试常问)](#3.3 建堆时间复杂度详细推导(面试常问))
[3.4 堆的插入操作](#3.4 堆的插入操作)
[3.5 堆的删除操作](#3.5 堆的删除操作)
四、手动模拟实现优先级队列(MyPriorityQueue)
[五、Java 官方 PriorityQueue 完整使用指南](#五、Java 官方 PriorityQueue 完整使用指南)
[5.1 PriorityQueue 核心特性(必背)](#5.1 PriorityQueue 核心特性(必背))
[5.2 常用构造方法](#5.2 常用构造方法)
[5.3 常用核心 API](#5.3 常用核心 API)
[5.4 如何创建大根堆(面试高频)](#5.4 如何创建大根堆(面试高频))
[5.5 JDK 1.8 扩容机制(源码级)](#5.5 JDK 1.8 扩容机制(源码级))
[6.1 堆排序](#6.1 堆排序)
[6.2 Top-K 问题(面试 / 笔试最高频)](#6.2 Top-K 问题(面试 / 笔试最高频))
[七、配套习题 + 详细答案](#七、配套习题 + 详细答案)
[习题 1](#习题 1)
[习题 2](#习题 2)
[习题 3](#习题 3)
[习题 4](#习题 4)
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在数据结构与算法体系中,优先级队列 是解决 "按优先级而非先后顺序出队" 场景的核心结构,而它的底层基石就是堆。无论是面试高频考点、算法题(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超出数组范围(到叶子节点)
完整代码实现
java
/**
* 小堆的向下调整
* @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)
- 每个节点都执行一次向下调整
建堆代码
java
/**
* 将普通数组调整为堆
*/
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 堆的插入操作
堆的插入必须保证插入后依然是堆。
插入步骤
- 把新元素放到数组末尾(完全二叉树最后一个位置)
- 对这个新元素执行向上调整,直到满足堆性质
向上调整代码(小堆)
java
/**
* 小堆向上调整
* @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)
结合上面的插入、删除、获取堆顶,我们可以写出一个最简可用的优先级队列(不考虑复杂扩容):
java
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 内置了开箱即用的优先级队列:java.util.PriorityQueue。
5.1 PriorityQueue 核心特性(必背)
- 必须导包:
import java.util.PriorityQueue; - 元素必须可比较 ,否则抛出
ClassCastException - 不能插入 null ,否则抛出
NullPointerException - 无容量上限,会自动扩容
- 插入 / 删除时间复杂度:O(logn)
- 底层是堆 ,默认是小根堆
PriorityQueue线程不安全;线程安全用PriorityBlockingQueue
5.2 常用构造方法
| 构造方法 | 说明 |
|---|---|
PriorityQueue() |
创建空优先级队列,默认容量 11 |
PriorityQueue(int initialCapacity) |
指定初始容量(不能小于 1) |
PriorityQueue(Collection<? extends E> c) |
使用集合直接创建堆 |
示例:
java
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() |
判断是否为空 |
示例:
java
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)。
java
// 自定义比较器:实现大根堆
class IntDescComparator implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
// 降序:o2 - o1
return o2 - 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 自动扩容规则:
java
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 → 向下调整堆顶
6.2 Top-K 问题(面试 / 笔试最高频)
问题描述 :在海量数据中,找出前 K 个最大 / 最小的数。比如:世界 500 强、成绩前 10 名、游戏战力前 100。
最优解法:堆(数据太大无法全部加载到内存时,只有堆能高效解决)
核心思路:
- 求前 K 个最大元素 → 建小根堆
- 求前 K 个最小元素 → 建大根堆
简单版代码(最小 K 个数):
java
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;
}
}
七、配套习题 + 详细答案
习题 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数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!