目录
[一、LeetCode 347. 前 K 个高频元素(中等)](#一、LeetCode 347. 前 K 个高频元素(中等))
[方法 1:小顶堆(推荐,时间复杂度 O (n log k))](#方法 1:小顶堆(推荐,时间复杂度 O (n log k)))
[方法 2:大顶堆(写法简单,但效率略低)](#方法 2:大顶堆(写法简单,但效率略低))
[代码实现(Java 小顶堆版)](#代码实现(Java 小顶堆版))
[二、LeetCode 295. 数据流的中位数(困难)](#二、LeetCode 295. 数据流的中位数(困难))
这两道题是堆(优先队列)的「面试双杀」,也是我自己二刷时的重点复盘题,帮你把核心模板和易错点一次性吃透。
一、LeetCode 347. 前 K 个高频元素(中等)
题目描述
给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
核心思路
这道题的核心是 **「统计频率 + 用堆筛选前 K 大」**,二刷时一定要掌握两种写法的区别:
方法 1:小顶堆(推荐,时间复杂度 O (n log k))
- 统计频率:用 HashMap 统计每个数字出现的次数。
- 维护小顶堆 :堆的大小始终不超过
k。遍历所有频率,当堆大小小于k时直接入堆;当堆大小等于k时,如果当前频率大于堆顶元素,则弹出堆顶、压入当前元素。 - 结果收集 :遍历结束后,堆中剩下的
k个元素就是频率最高的前k个。
方法 2:大顶堆(写法简单,但效率略低)
直接把所有元素按频率压入大顶堆,然后弹出前 k 个即可。但时间复杂度为 O (n log n),在数据量大时效率不如小顶堆。
代码实现(Java 小顶堆版)
java
运行
public int[] topKFrequent(int[] nums, int k) {
// 1. 统计频率
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
// 2. 小顶堆:按频率升序排列,堆顶是当前频率最小的元素
PriorityQueue<Map.Entry<Integer, Integer>> minHeap =
new PriorityQueue<>(Comparator.comparingInt(Map.Entry::getValue));
for (Map.Entry<Integer, Integer> entry : freqMap.entrySet()) {
if (minHeap.size() < k) {
minHeap.offer(entry);
} else {
if (entry.getValue() > minHeap.peek().getValue()) {
minHeap.poll();
minHeap.offer(entry);
}
}
}
// 3. 收集结果
int[] res = new int[k];
int idx = 0;
while (!minHeap.isEmpty()) {
res[idx++] = minHeap.poll().getKey();
}
return res;
}
二刷复盘要点
- 为什么用小顶堆而不是大顶堆?因为小顶堆的大小始终是
k,堆操作的时间复杂度是 O (log k),整体效率更高。 - 易错点:堆的比较器要写对,小顶堆是按频率升序,而不是按元素本身的值排序。
二、LeetCode 295. 数据流的中位数(困难)
题目描述
中位数是有序列表中间的数。如果列表长度是偶数,中位数是中间两个数的平均值。设计一个支持 addNum 和 findMedian 操作的数据结构:
addNum(int num)从数据流中添加一个整数到数据结构中。findMedian()返回目前所有元素的中位数。
核心思路:双堆法(经典模板)
用两个堆分别维护数据的左右两半:
- 大顶堆(left):存储前半部分数据,堆顶是前半部分的最大值。
- 小顶堆(right):存储后半部分数据,堆顶是后半部分的最小值。
维护规则:
- 大顶堆的大小要么和小顶堆相等,要么比小顶堆多 1(保证奇数长度时,大顶堆的堆顶就是中位数)。
- 每次添加元素时,先根据元素大小判断该加入哪个堆,再调整两个堆的大小,保持平衡。
代码实现(Java)
java
运行
class MedianFinder {
// 大顶堆:存储前半部分,堆顶是前半部分的最大值
private PriorityQueue<Integer> left;
// 小顶堆:存储后半部分,堆顶是后半部分的最小值
private PriorityQueue<Integer> right;
public MedianFinder() {
left = new PriorityQueue<>((a, b) -> b - a);
right = new PriorityQueue<>();
}
public void addNum(int num) {
// 先决定加入哪个堆
if (left.isEmpty() || num <= left.peek()) {
left.offer(num);
} else {
right.offer(num);
}
// 调整堆的大小,保持平衡
// 情况1:大顶堆比小顶堆多2个,需要把大顶堆的堆顶移到小顶堆
if (left.size() > right.size() + 1) {
right.offer(left.poll());
}
// 情况2:小顶堆比大顶堆多,需要把小顶堆的堆顶移到大顶堆
else if (right.size() > left.size()) {
left.offer(right.poll());
}
}
public double findMedian() {
// 奇数个元素,大顶堆的堆顶就是中位数
if (left.size() > right.size()) {
return left.peek();
}
// 偶数个元素,取两个堆顶的平均值
else {
return (left.peek() + right.peek()) / 2.0;
}
}
}
二刷复盘要点
- 两个堆的作用是什么?大顶堆维护左半部分,小顶堆维护右半部分,这样两个堆顶就是整个数据的中间两个数,取中位数的时间复杂度是 O (1)。
- 易错点:堆的大小调整逻辑一定要写对,尤其是大顶堆和小顶堆的大小关系,否则会导致中位数计算错误。
- 面试常问:除了双堆法,还有没有其他方法?(如二分查找维护有序数组,但插入时间复杂度是 O (n),效率不如双堆法)
三、两道题的核心模板总结
表格
| 题目 | 核心数据结构 | 关键技巧 | 时间复杂度 |
|---|---|---|---|
| 前 K 个高频元素 | HashMap + 小顶堆 | 维护堆大小不超过 k,效率最优 | O(n log k) |
| 数据流的中位数 | 大顶堆 + 小顶堆 | 双堆平衡,堆顶直接取中位数 | addNum: O(log n), findMedian: O(1) |