Problem: 347. 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
整体思路
这段代码同样旨在解决 “前 K 个高频元素” 问题。此版本采用了一种 “哈希表 + 最大堆” 的组合方法。与使用最小堆维护 Top K 的方法不同,这个实现是将所有唯一元素都放入一个最大堆中,然后从中提取出最大的 k
个。
算法的整体思路可以清晰地分解为以下三个步骤:
-
第一步:频率统计
- 与之前的方法完全相同,算法首先使用一个 哈希表 (HashMap)
frequencyMap
来高效地统计数组nums
中每个数字出现的频率。 - 这一步为后续的排序提供了基础数据。
- 与之前的方法完全相同,算法首先使用一个 哈希表 (HashMap)
-
第二步:构建最大堆
- 这是算法的核心。它创建了一个 最大堆 (Max-Heap)。
- 堆中存储的内容:这个堆直接存储的是数字本身(即
frequencyMap
的键)。 - 比较逻辑:通过一个自定义的比较器
(a, b) -> frequencyMap.get(b) - frequencyMap.get(a)
,堆在进行内部排序时,会去frequencyMap
中查找每个数字对应的频率,并根据频率进行比较。frequencyMap.get(b) - frequencyMap.get(a)
实现了按频率降序排列,从而构成了一个最大堆。 - 填充堆:通过
maxHeap.addAll(frequencyMap.keySet())
,将frequencyMap
中所有唯一的数字(M
个)一次性地全部加入到最大堆中。Java 的PriorityQueue
在addAll
时会进行“堆化(heapify)”操作,这是一个 O(M) 的高效过程。
-
第三步:提取 Top K 结果
- 此时,最大堆的堆顶就是频率最高的元素,第二个是次高的,以此类推。
- 算法创建一个大小为
k
的结果数组ans
,然后通过一个for
循环,连续从最大堆中弹出(poll)k
次元素。 - 每次弹出的元素都是当前堆中频率最高的,将它们依次存入
ans
数组。 - 返回
ans
数组。
这个方法在逻辑上等价于先排序再取前k个,但利用了堆这种数据结构。相比于维护大小为k的最小堆的方法,它的时间复杂度略高,因为需要对所有 M
个唯一元素进行建堆操作。
完整代码
class Solution {
/**
* 找出数组中出现频率最高的前 k 个元素。
* @param nums 整数数组
* @param k 需要找出的元素个数
* @return 包含前 k 个高频元素的数组
*/
public int[] topKFrequent(int[] nums, int k) {
// 步骤 1: 使用 HashMap 统计每个数字的出现频率
Map<Integer, Integer> frequencyMap = new HashMap<>();
for (int num : nums) {
frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
}
// 步骤 2: 构建一个最大堆
// 堆中存储的是数字本身。
// 比较器通过查询 frequencyMap 来按频率进行降序比较,从而实现最大堆。
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> frequencyMap.get(b) - frequencyMap.get(a));
// 将所有唯一数字(map 的键集合)一次性加入最大堆。
// 这个操作会触发一个 O(M) 的堆化过程。
maxHeap.addAll(frequencyMap.keySet());
// 步骤 3: 从最大堆中提取前 k 个元素
int[] ans = new int[k];
for (int i = 0; i < k; i++) {
// 依次从堆中弹出频率最高的元素
ans[i] = maxHeap.poll();
}
return ans;
}
}
时空复杂度
时间复杂度:O(N + M log M)
- 频率统计:遍历
nums
数组一次,时间复杂度为 O(N)。 - 构建最大堆:
maxHeap.addAll(frequencyMap.keySet())
:将M
个唯一元素加入优先队列。这个批量添加操作在 Java 中被优化为“堆化(heapify)”,其时间复杂度为 O(M)。- 备选分析:如果逐个添加
M
个元素,总时间复杂度会是O(M log M)
。但addAll
通常更高效。
- 提取结果:
- 从堆中
poll
出k
个元素。每次poll
操作都需要 O(log M) 的时间(因为堆的大小从M
逐渐减小到M-k
)。 - 因此,这部分的总时间复杂度是 O(k log M)。
- 从堆中
综合分析:
总时间复杂度 = O(N) (统计) + O(M) (建堆) + O(k log M) (提取)。
由于 k <= M <= N
,其中 O(k log M)
这一项在 k
较小时可能不是主导项,但为了覆盖所有情况,我们需要考虑它。
因此,总的时间复杂度可以写为 O(N + k log M)。
如果考虑到最坏情况,即 k
接近 M
,则复杂度变为 O(N + M log M),这与完全排序的时间复杂度相当。
空间复杂度:O(M)
- 主要存储开销:
HashMap frequencyMap
: 需要存储所有M
个唯一元素及其频率。空间为 O(M)。PriorityQueue maxHeap
: 需要存储这M
个唯一数字。空间为 O(M)。
综合分析:
算法所需的额外空间由哈希表和最大堆共同构成,两者的大小都与唯一元素的数量 M
成正比。因此,总的空间复杂度为 O(M)。