堆与 Top K 模板:前 K、动态最值、优先级问题的标准写法#
堆的本质就是:
始终把“当前最重要的元素”放在最前面。
当题目出现“前 K”“实时最值”“每次都要取当前最大/最小”“优先处理某个元素”时,堆通常就是最稳的选择。
⚡ 速记版模板#
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 小顶堆:堆顶是当前堆中最小元素
for (int num : nums) { // 逐个处理数组元素
if (minHeap.size() < k) { // 堆未满,先直接加入
minHeap.offer(num);
} else if (num > minHeap.peek()) { // 若当前元素比堆顶大,说明更有资格进入 TopK
minHeap.poll(); // 先移除 TopK 中最小的那个
minHeap.offer(num); // 再加入当前更大的元素
}
}
return minHeap.peek(); // 维护完后,堆顶即第 k 大元素java🎯 什么时候想到堆?#
典型信号:
- 第 K 大 / 第 K 小
- 前 K 个高频元素
- 数据流中的中位数
- 合并多个有序结构
- 每次需要快速取出当前最大值或最小值
Hot 100 里的典型题目:
💡 Java 里的堆怎么写?#
Java 用 PriorityQueue:
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 小顶堆:peek() 始终是最小值
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a); // 大顶堆:通过比较器反转顺序,peek() 是最大值java- 默认是小顶堆
- 想要大顶堆,就传比较器
🚀 模板一:固定大小为 K 的小顶堆#
适合:
- 第 K 大元素
- Top K 类问题
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // 固定容量 k 的小顶堆
for (int num : nums) { // 扫描所有元素
if (minHeap.size() < k) { // 堆未满,直接入堆
minHeap.offer(num);
} else if (num > minHeap.peek()) { // 仅当当前元素更大时才更新 TopK
minHeap.poll(); // 移除 TopK 中最小值
minHeap.offer(num); // 加入更大元素
}
}
return minHeap.peek(); // 堆顶就是最终第 k 大
}
}java思路:
- 堆里始终保留当前最大的
k个元素 - 堆顶就是这
k个里最小的那个 - 也就是第
k大
🚀 模板二:按频率排序的堆#
适合:
- 前 K 个高频元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>(); // 统计每个数字出现频次
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1); // 频次 +1
}
PriorityQueue<Integer> minHeap = new PriorityQueue<>(
(a, b) -> count.get(a) - count.get(b) // 频次小的优先出堆,便于维护“频次最高的 k 个”
);
for (int num : count.keySet()) { // 遍历去重后的数字集合
if (minHeap.size() < k) { // 堆未满先放入
minHeap.offer(num);
} else if (count.get(num) > count.get(minHeap.peek())) { // 当前数字频次更高,替换堆顶
minHeap.poll();
minHeap.offer(num);
}
}
int[] answer = new int[k]; // 输出数组
for (int index = k - 1; index >= 0; index--) { // 逆序填充,让高频元素在前(可选)
answer[index] = minHeap.poll(); // 依次弹出堆中元素
}
return answer; // 返回前 k 个高频元素
}
}java🚀 模板三:多路归并堆#
适合:
- 合并 K 个有序链表
- 合并 K 个有序数组
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> minHeap = new PriorityQueue<>(
(a, b) -> a.val - b.val // 按节点值从小到大排列
);
for (ListNode node : lists) { // 先把每条链表的头节点入堆
if (node != null) { // 过滤空链表
minHeap.offer(node);
}
}
ListNode dummy = new ListNode(0); // 虚拟头节点,简化拼接逻辑
ListNode tail = dummy; // 已合并链表的尾指针
while (!minHeap.isEmpty()) { // 每次弹出当前最小节点
ListNode current = minHeap.poll(); // 该节点应接到结果链表末尾
tail.next = current;
tail = tail.next;
if (current.next != null) { // 将被弹出节点所在链表的下一个节点继续入堆
minHeap.offer(current.next);
}
}
return dummy.next; // 返回合并后链表头
}
}java🚀 模板四:双堆结构#
适合:
- 数据流中的中位数
- 动态维护两半数据
class MedianFinder {
private PriorityQueue<Integer> smallerHalf; // 大顶堆:保存较小的一半数据,堆顶是这半中的最大值
private PriorityQueue<Integer> largerHalf; // 小顶堆:保存较大的一半数据,堆顶是这半中的最小值
public MedianFinder() {
smallerHalf = new PriorityQueue<>((a, b) -> b - a); // 初始化大顶堆
largerHalf = new PriorityQueue<>(); // 初始化小顶堆
}
public void addNum(int num) {
if (smallerHalf.isEmpty() || num <= smallerHalf.peek()) { // 新数更小,先放入左半边
smallerHalf.offer(num);
} else { // 新数更大,放入右半边
largerHalf.offer(num);
}
if (smallerHalf.size() > largerHalf.size() + 1) { // 左边过多:把左边最大值移动到右边
largerHalf.offer(smallerHalf.poll());
} else if (largerHalf.size() > smallerHalf.size()) { // 右边过多:把右边最小值移动到左边
smallerHalf.offer(largerHalf.poll());
}
}
public double findMedian() {
if (smallerHalf.size() > largerHalf.size()) { // 总数为奇数时,左边多一个
return smallerHalf.peek(); // 中位数就是左堆堆顶
}
return (smallerHalf.peek() + largerHalf.peek()) / 2.0; // 总数为偶数时,取两堆顶平均
}
}java⚠️ 易错点#
-
什么时候用小顶堆,什么时候用大顶堆
- Top K 最大值:常用小顶堆
- Top K 最小值:常用大顶堆
-
比较器别写反
- 比较器决定堆顶是什么元素
-
不要忘记维护堆大小
- 固定大小堆的关键就是及时弹出
-
链表或对象入堆时要明确比较字段
- 不要只会写整数堆
🎨 面试时怎么说#
你可以这样说:
这题本质上是一个优先级维护问题,我用堆保证每次都能在
O(logn)或O(logk)时间拿到当前最关键的元素。
📌 一句话总结#
堆最适合处理两类问题:
- 你总想先拿到当前最大 / 最小元素
- 你只关心前 K 名,不关心完整排序