"在优先队列的世界里,不是先来先得,而是优先级高的先走!" 🎯
📖 一、什么是优先队列?从医院急诊说起
1.1 生活中的场景
想象你在医院急诊室:
普通队列(FIFO):
张三(感冒)→ 李四(发烧)→ 王五(肚子疼)
按到达顺序就诊,谁先来谁先看
优先队列(Priority Queue):
患者A(轻微擦伤,优先级1)
患者B(骨折,优先级5) ← B先看!(优先级高)
患者C(心脏病发,优先级10) ← C最先看!(最高优先级)
即使C最后到,但因为优先级最高,会最先被处理!
1.2 专业定义
优先队列(Priority Queue) 是一种特殊的队列,每个元素都有一个优先级,出队时总是**优先级最高(或最低)**的元素先出队,而不管入队的先后顺序。
核心特点:
- ✅ 不是先进先出(FIFO),而是优先级决定顺序
- ✅ 底层通常用**堆(Heap)**实现
- ✅ Java的
PriorityQueue默认是最小堆(最小值优先) - ⚡ 入队和出队的时间复杂度都是 O(log n)
🏔️ 二、堆(Heap):优先队列的秘密武器
2.1 什么是堆?
堆是一种特殊的完全二叉树,满足以下性质:
🔻 最小堆(Min Heap)
- 父节点的值 ≤ 子节点的值
- 堆顶(根节点)是最小值
- Java的
PriorityQueue默认就是最小堆
🔺 最大堆(Max Heap)
- 父节点的值 ≥ 子节点的值
- 堆顶(根节点)是最大值
2.2 最小堆图解
1 ← 堆顶(最小值)
/ \
3 2 ← 第二层
/ \ / \
5 4 6 7 ← 第三层
特点:
- 1 < 3, 1 < 2 (父 < 子)
- 3 < 5, 3 < 4
- 2 < 6, 2 < 7
2.3 堆用数组存储
堆虽然是树结构,但用数组存储超级方便!
数组索引: 0 1 2 3 4 5 6
数组值: [1, 3, 2, 5, 4, 6, 7]
1(0)
/ \
3(1) 2(2)
/ \ / \
5(3) 4(4) 6(5) 7(6)
重要公式(索引从0开始):
- 父节点索引:(i - 1) / 2
- 左子节点索引:2 * i + 1
- 右子节点索引:2 * i + 2
2.4 堆的核心操作
🔼 上浮(Swim/Bubble Up)
插入新元素时,先放到数组末尾,然后不断与父节点比较,如果比父节点小就交换,直到满足堆性质。
插入元素 0 到堆中:
步骤1:先放到末尾
1
/ \
3 2
/ \ / \
5 4 6 7
/
0 ← 新插入
步骤2:0 < 5,交换
1
/ \
3 2
/ \ / \
0 4 6 7
/
5
步骤3:0 < 3,继续交换
1
/ \
0 2
/ \ / \
3 4 6 7
/
5
步骤4:0 < 1,继续交换
0 ← 最终成为堆顶
/ \
1 2
/ \ / \
3 4 6 7
/
5
🔽 下沉(Sink/Bubble Down)
删除堆顶元素时,用末尾元素替换堆顶,然后不断与子节点比较,与最小的子节点交换,直到满足堆性质。
删除堆顶元素 1:
步骤1:用末尾7替换堆顶1
7 ← 末尾元素上来
/ \
3 2
/ \ /
5 4 6
步骤2:7 > 2(最小子节点),交换
2
/ \
3 7
/ \ /
5 4 6
步骤3:7 > 6(最小子节点),交换
2
/ \
3 6
/ \ /
5 4 7 ← 完成!
💻 三、Java中的PriorityQueue
3.1 基本使用
import java.util.*;
public class PriorityQueueDemo {
public static void main(String[] args) {
// 创建最小堆(默认)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 添加元素
minHeap.offer(5);
minHeap.offer(3);
minHeap.offer(7);
minHeap.offer(1);
minHeap.offer(9);
System.out.println("=== 按优先级出队(从小到大)===");
while (!minHeap.isEmpty()) {
System.out.print(minHeap.poll() + " ");
}
// 输出:1 3 5 7 9
}
}
3.2 创建最大堆
// 方法1:使用Collections.reverseOrder()
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// 方法2:使用Lambda表达式
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>((a, b) -> b - a);
// 测试
maxHeap.offer(5);
maxHeap.offer(3);
maxHeap.offer(7);
maxHeap.offer(1);
System.out.println(maxHeap.poll()); // 7(最大值先出)
System.out.println(maxHeap.poll()); // 5
System.out.println(maxHeap.poll()); // 3
3.3 自定义对象的优先队列
// 定义学生类
class Student {
String name;
int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return name + "(" + score + "分)";
}
}
public class CustomPriorityQueue {
public static void main(String[] args) {
// 按分数从高到低排序(分数高的优先)
PriorityQueue<Student> pq = new PriorityQueue<>(
(s1, s2) -> s2.score - s1.score
);
pq.offer(new Student("张三", 85));
pq.offer(new Student("李四", 92));
pq.offer(new Student("王五", 78));
pq.offer(new Student("赵六", 95));
System.out.println("=== 按分数从高到低出队 ===");
while (!pq.isEmpty()) {
System.out.println(pq.poll());
}
// 输出:
// 赵六(95分)
// 李四(92分)
// 张三(85分)
// 王五(78分)
}
}
3.4 常用API总结
| 方法 | 说明 | 时间复杂度 |
|---|---|---|
offer(e) / add(e) | 插入元素 | O(log n) |
poll() / remove() | 删除并返回堆顶 | O(log n) |
peek() / element() | 查看堆顶(不删除) | O(1) |
size() | 返回元素个数 | O(1) |
isEmpty() | 判断是否为空 | O(1) |
clear() | 清空队列 | O(n) |
contains(o) | 是否包含元素 | O(n) |
🎯 四、经典应用场景
4.1 Top K 问题 🏆
问题: 从海量数据中找出最大的K个元素
解法: 用大小为K的最小堆!
public class TopKElements {
public static int[] findTopK(int[] nums, int k) {
// 使用最小堆,保持堆大小为k
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll(); // 移除最小的
}
}
// 堆中剩下的就是Top K
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = minHeap.poll();
}
return result;
}
public static void main(String[] args) {
int[] nums = {3, 2, 1, 5, 6, 4, 9, 7, 8};
int k = 3;
int[] topK = findTopK(nums, k);
System.out.println("Top " + k + ": " + Arrays.toString(topK));
// 输出:Top 3: [7, 8, 9]
}
}
为什么用最小堆?
找最大的K个元素,用最小堆:
- 堆顶是K个元素中最小的
- 新元素如果比堆顶大,就替换堆顶
- 最终堆里都是大元素
找最小的K个元素,用最大堆:
- 堆顶是K个元素中最大的
- 新元素如果比堆顶小,就替换堆顂
- 最终堆里都是小元素
4.2 合并K个有序链表 🔗
LeetCode 23 - Hard题
public class MergeKSortedLists {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
// 最小堆,按节点值排序
PriorityQueue<ListNode> pq = new PriorityQueue<>(
(a, b) -> a.val - b.val
);
// 将每个链表的头节点加入堆
for (ListNode node : lists) {
if (node != null) {
pq.offer(node);
}
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (!pq.isEmpty()) {
// 取出最小的节点
ListNode minNode = pq.poll();
curr.next = minNode;
curr = curr.next;
// 如果该链表还有下一个节点,加入堆
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
return dummy.next;
}
}
过程演示:
链表1: 1 → 4 → 5
链表2: 1 → 3 → 4
链表3: 2 → 6
步骤1:堆中放入三个头节点
堆:[1(链表1), 1(链表2), 2(链表3)]
步骤2:取出1(链表1),加入4
堆:[1(链表2), 2(链表3), 4(链表1)]
步骤3:取出1(链表2),加入3
堆:[2(链表3), 3(链表2), 4(链表1)]
...以此类推
最终结果:1 → 1 → 2 → 3 → 4 → 4 → 5 → 6
4.3 任务调度器 ⏰
模拟操作系统的任务调度:
class Task {
String name;
int priority; // 数字越大优先级越高
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public String toString() {
return name + "(P" + priority + ")";
}
}
public class TaskScheduler {
public static void main(String[] args) {
// 最大堆:优先级高的先执行
PriorityQueue<Task> scheduler = new PriorityQueue<>(
(t1, t2) -> t2.priority - t1.priority
);
// 添加任务
scheduler.offer(new Task("打游戏", 1));
scheduler.offer(new Task("写代码", 5));
scheduler.offer(new Task("修复Bug", 10));
scheduler.offer(new Task("开会", 7));
scheduler.offer(new Task("吃饭", 8));
System.out.println("=== 任务执行顺序 ===");
int order = 1;
while (!scheduler.isEmpty()) {
Task task = scheduler.poll();
System.out.println(order++ + ". " + task);
}
}
}
输出:
=== 任务执行顺序 ===
1. 修复Bug(P10) ← 最高优先级
2. 吃饭(P8)
3. 开会(P7)
4. 写代码(P5)
5. 打游戏(P1) ← 最低优先级
4.4 数据流中的中位数 📊
LeetCode 295 - Hard题
使用两个堆:
- 最大堆:存储较小的一半
- 最小堆:存储较大的一半
class MedianFinder {
// 最大堆:存储较小的一半数据
private PriorityQueue<Integer> maxHeap;
// 最小堆:存储较大的一半数据
private PriorityQueue<Integer> minHeap;
public MedianFinder() {
maxHeap = new PriorityQueue<>((a, b) -> b - a);
minHeap = new PriorityQueue<>();
}
public void addNum(int num) {
// 先加入最大堆
maxHeap.offer(num);
// 将最大堆的堆顶移到最小堆
minHeap.offer(maxHeap.poll());
// 保持最大堆的大小 >= 最小堆
if (maxHeap.size() < minHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() > minHeap.size()) {
return maxHeap.peek();
} else {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
}
}
}
// 测试
MedianFinder mf = new MedianFinder();
mf.addNum(1);
mf.addNum(2);
System.out.println(mf.findMedian()); // 1.5
mf.addNum(3);
System.out.println(mf.findMedian()); // 2.0
图解:
加入数据:1, 2, 3, 4, 5
最大堆(小的一半) | 最小堆(大的一半)
3, 2, 1 | 4, 5
↑ ↑
堆顶最大 堆顶最小
中位数 = 3(最大堆堆顶)
4.5 实时股票价格监控 📈
class StockPrice {
String symbol;
double price;
public StockPrice(String symbol, double price) {
this.symbol = symbol;
this.price = price;
}
}
public class StockMonitor {
public static void main(String[] args) {
// 最大堆:价格最高的股票
PriorityQueue<StockPrice> highestPrices = new PriorityQueue<>(
(s1, s2) -> Double.compare(s2.price, s1.price)
);
// 添加股票价格
highestPrices.offer(new StockPrice("AAPL", 175.50));
highestPrices.offer(new StockPrice("GOOGL", 140.30));
highestPrices.offer(new StockPrice("MSFT", 380.20));
highestPrices.offer(new StockPrice("AMZN", 155.80));
System.out.println("价格最高的股票:");
StockPrice highest = highestPrices.peek();
System.out.println(highest.symbol + ": $" + highest.price);
// 输出:MSFT: $380.20
}
}
🎓 五、经典面试题
面试题1:PriorityQueue底层用什么实现?
答案:
- 底层用数组存储
- 逻辑结构是完全二叉树(堆)
- Java的PriorityQueue默认是最小堆
- 初始容量是11,扩容1.5倍或2倍(取决于当前容量)
面试题2:为什么堆的插入和删除都是O(log n)?
答案:
- 堆是完全二叉树,高度为 log n
- 插入:上浮操作最多交换 log n 次
- 删除:下沉操作最多交换 log n 次
面试题3:如何在O(n)时间内建堆?
答案: 使用heapify(堆化)算法:
- 从最后一个非叶子节点开始,从下往上执行下沉操作
- 时间复杂度:O(n)(虽然看起来是O(n log n),但实际数学证明是O(n))
public void heapify(int[] arr) {
int n = arr.length;
// 从最后一个非叶子节点开始
for (int i = n / 2 - 1; i >= 0; i--) {
siftDown(arr, i, n);
}
}
面试题4:PriorityQueue是线程安全的吗?
答案:
- ❌ 不是线程安全的
- 如需线程安全,使用
PriorityBlockingQueue(并发包中的实现)
面试题5:如何找出数据流中的第K大元素?
答案: 维护一个大小为K的最小堆:
class KthLargest {
private PriorityQueue<Integer> minHeap;
private int k;
public KthLargest(int k, int[] nums) {
this.k = k;
this.minHeap = new PriorityQueue<>();
for (int num : nums) {
add(num);
}
}
public int add(int val) {
minHeap.offer(val);
if (minHeap.size() > k) {
minHeap.poll();
}
return minHeap.peek();
}
}
🆚 六、优先队列 vs 普通队列
| 特性 | 普通队列(Queue) | 优先队列(PriorityQueue) |
|---|---|---|
| 出队顺序 | FIFO(先进先出) | 按优先级 |
| 底层实现 | 数组/链表 | 堆(数组) |
| 插入时间 | O(1) | O(log n) |
| 删除时间 | O(1) | O(log n) |
| 查找最值 | O(n) | O(1) |
| 适用场景 | 任务排队 | 需要按优先级处理 |
🎨 七、手写最小堆
理解底层原理,手写一个简单的最小堆:
public class MinHeap {
private int[] heap;
private int size;
private int capacity;
public MinHeap(int capacity) {
this.capacity = capacity;
this.heap = new int[capacity];
this.size = 0;
}
// 获取父节点索引
private int parent(int i) {
return (i - 1) / 2;
}
// 获取左子节点索引
private int leftChild(int i) {
return 2 * i + 1;
}
// 获取右子节点索引
private int rightChild(int i) {
return 2 * i + 2;
}
// 交换两个元素
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
// 上浮操作
private void siftUp(int i) {
while (i > 0 && heap[i] < heap[parent(i)]) {
swap(i, parent(i));
i = parent(i);
}
}
// 下沉操作
private void siftDown(int i) {
int minIndex = i;
int left = leftChild(i);
int right = rightChild(i);
if (left < size && heap[left] < heap[minIndex]) {
minIndex = left;
}
if (right < size && heap[right] < heap[minIndex]) {
minIndex = right;
}
if (i != minIndex) {
swap(i, minIndex);
siftDown(minIndex);
}
}
// 插入元素
public void insert(int value) {
if (size == capacity) {
throw new IllegalStateException("Heap is full");
}
heap[size] = value;
siftUp(size);
size++;
}
// 删除并返回最小元素
public int extractMin() {
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
int min = heap[0];
heap[0] = heap[size - 1];
size--;
siftDown(0);
return min;
}
// 查看最小元素
public int peek() {
if (size == 0) {
throw new IllegalStateException("Heap is empty");
}
return heap[0];
}
// 测试
public static void main(String[] args) {
MinHeap heap = new MinHeap(10);
heap.insert(5);
heap.insert(3);
heap.insert(7);
heap.insert(1);
heap.insert(9);
System.out.println("最小值: " + heap.peek()); // 1
System.out.println("出队: " + heap.extractMin()); // 1
System.out.println("出队: " + heap.extractMin()); // 3
System.out.println("出队: " + heap.extractMin()); // 5
}
}
🎪 八、趣味小故事
故事:优先队列的VIP人生
我是优先队列PriorityQueue,江湖人称"VIP队列" 👑
我的工作地点是一家高级餐厅,专门负责安排客人入座顺序。
有一天,来了这些客人:
- 👨 普通客人张三(优先级1)
- 👨💼 VIP客人李四(优先级5)
- 👑 超级VIP王五(优先级10)
- 👴 老年人赵六(优先级8)
虽然张三最先到,但我的规则是:优先级高的先服务!
所以实际的入座顺序是:
- 👑 王五(优先级10)- "超级VIP请进!"
- 👴 赵六(优先级8)- "尊老爱幼,请!"
- 👨💼 李四(优先级5)- "VIP客人请!"
- 👨 张三(优先级1)- "不好意思,请稍等..."
张三不服:"我明明先来的!"
我说:"在我的世界里,不是先来先得,而是优先级决定一切!这就是优先队列的规则!" 😎
后来,我还帮助了很多场景:
- 🏥 医院急诊室(病情严重的先治)
- 💻 操作系统任务调度(重要任务先执行)
- 📊 数据分析(找Top K)
- 🎮 游戏开发(技能冷却队列)
虽然有人说我"势利眼",但在很多场景下,按优先级处理才是最合理的!💪
📚 九、知识点总结
核心要点 ✨
- 定义:优先队列按优先级出队,不是FIFO
- 实现:底层用堆(数组存储的完全二叉树)
- 类型:最小堆(小值优先)、最大堆(大值优先)
- 时间复杂度:插入O(log n)、删除O(log n)、查看堆顶O(1)
- Java实现:PriorityQueue(非线程安全)
- 经典应用:Top K、任务调度、数据流中位数
记忆口诀 🎵
优先队列不排队,
优先级高先出队。
底层实现是个堆,
完全二叉树排队。
最小堆顶是最小,
最大堆顶是最大。
插入删除log n快,
Top K问题最常来!
堆的核心公式 📐
// 数组索引从0开始
父节点索引 = (i - 1) / 2
左子节点索引 = 2 * i + 1
右子节点索引 = 2 * i + 2
使用场景对照表 🎯
| 场景 | 使用什么堆 | 原因 |
|---|---|---|
| 找最大的K个 | 最小堆(size=K) | 堆顶最小,新元素比堆顶大就替换 |
| 找最小的K个 | 最大堆(size=K) | 堆顶最大,新元素比堆顶小就替换 |
| 任务调度 | 最大堆 | 优先级高的先执行 |
| 合并K个链表 | 最小堆 | 每次取最小的节点 |
🎯 十、练习题
练习1:数组中的第K个最大元素(LeetCode 215)
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
return minHeap.peek();
}
练习2:前K个高频元素(LeetCode 347)
public int[] topKFrequent(int[] nums, int k) {
// 统计频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 最小堆,按频率排序
PriorityQueue<Map.Entry<Integer, Integer>> minHeap =
new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());
for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
minHeap.offer(entry);
if (minHeap.size() > k) {
minHeap.poll();
}
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) {
result[i] = minHeap.poll().getKey();
}
return result;
}
🌟 十一、总结彩蛋
恭喜你!🎉 你已经掌握了优先队列这个超级实用的数据结构!
记住:
- 🎯 优先队列 = 按优先级处理,不是FIFO
- 🏔️ 底层用堆,数组存储的完全二叉树
- ⚡ O(log n) 的插入删除,O(1) 查看堆顶
- 🔥 Top K问题的神器!
最后送你一张图
👑
/ \
/ \
VIP 普通
/ \ / \
💎 ⭐ 🌟 ✨
优先队列:让重要的先走!
下次见,继续加油! 💪😄
📖 参考资料
- Java官方文档:PriorityQueue
- 《算法导论》第6章 - 堆排序
- 《数据结构与算法分析》- 堆的应用
- LeetCode题单:堆/优先队列专题
作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐⭐ (中高级)
预计学习时间: 3-4小时
💡 温馨提示:理解堆的上浮和下沉操作是关键,建议画图模拟整个过程!