题目描述
给定一个整数数组 nums 和一个整数 k,返回其中出现频率前 k 高的元素。
答案可以按任意顺序返回。
题目保证 k 在不同元素的数量范围内,并且答案唯一。
例如:
nums = [1,1,1,2,2,3], k = 2
每个元素的出现频率是:
1 -> 3 次
2 -> 2 次
3 -> 1 次
所以返回:
[1, 2]
注意:题目要返回的是出现频率最高的元素本身,不是它们的出现次数。
最初思路
一开始的想法是:
- 用
HashMap统计每个元素出现的次数。 - 把频率取出来排序。
- 取排序后的前
k个结果。
这个方向里,统计频率这一步是对的:
Map<Integer, Integer> m = new HashMap<>();
for (int num : nums) {
m.merge(num, 1, Integer::sum);
}
但后面如果只把 value 放进数组排序,最后得到的是频率,不是元素本身。
比如:
nums = [1,1,1,2,2,3], k = 2
频率数组是:
[3, 2, 1]
取前两个会得到:
[3, 2]
但正确答案应该是:
[1, 2]
问题出在哪里
这道题最容易混淆的是 Map 里的 key 和 value:
key -> 元素本身
value -> 出现频率
题目要求返回的是 key,只是排序依据是 value。
所以不能只对频率排序,然后把频率放进答案数组。真正要做的是:根据频率找到对应的元素。
后来改成桶排序时,又出现了几个细节问题:
new ArrayList[maxCnt + 1]只创建了数组,每个桶还没有初始化。- 从高频往低频遍历时,循环条件不能写成
i < k,因为i表示频率,不表示已经取了几个元素。 - 应该用
j < k控制答案数量,取满k个元素后停止遍历。
正确思路
可以用桶排序来做。
核心思想是:频率最大不会超过 nums.length,所以可以创建一个桶数组,让下标表示频率。
buckets[频率] = 这个频率下的所有元素
例如:
nums = [1,1,1,2,2,3]
统计频率后:
1 -> 3
2 -> 2
3 -> 1
放入桶中:
buckets[3] = [1]
buckets[2] = [2]
buckets[1] = [3]
然后从最高频率开始往低频率遍历,依次把元素放进答案数组,直到取满 k 个。
关键不变量
桶排序过程中要始终保持:
buckets[cnt] 里存放的都是出现次数为 cnt 的元素
答案收集过程中要始终保持:
j 表示 ans 中已经放入的元素个数
所以外层循环用频率 i 定位桶,同时用 j < k 控制答案数量。
手推过程
以这个例子为例:
nums = [1,1,1,2,2,3], k = 2
第一步,统计频率:
1 -> 3
2 -> 2
3 -> 1
第二步,放入桶:
buckets[3] = [1]
buckets[2] = [2]
buckets[1] = [3]
第三步,从高频到低频取元素:
i = 3,取出 1,ans = [1]
i = 2,取出 2,ans = [1, 2]
此时已经取满 k = 2 个元素,直接结束。
边界处理
题目保证答案唯一。因此在频率分界处,不会出现“同一个桶里只需要取一部分元素”的情况:如果某个频率对应多个元素,那么这些元素要么全部属于前 k 个高频元素,要么全部不属于。
所以只需要在外层循环中使用 j < k 控制答案数量:
for (int i = maxCnt; j < k; i--) {
for (int x : list[i]) {
ans[j++] = x;
}
}
同时,题目保证 k 在有效范围内,因此一定可以收集到 k 个元素,i 不会在答案收集完成前越界。
伪代码
创建 HashMap freq
遍历 nums:
freq[num]++
找到最大频率 maxCnt
创建 buckets,长度为 maxCnt + 1
初始化每一个桶
遍历 freq:
num = entry.key
cnt = entry.value
buckets[cnt].add(num)
创建答案数组 ans
j = 0
从 maxCnt 遍历到 1:
遍历 buckets[i] 中的元素:
ans[j] = 当前元素
j++
如果已经收集 k 个元素:
停止
返回 ans
Java 代码
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.merge(num, 1, Integer::sum);
}
int maxCnt = Collections.max(freq.values());
List<Integer>[] list = new ArrayList[maxCnt + 1];
for (int i = 0; i <= maxCnt; i++) {
list[i] = new ArrayList<>();
}
for (Map.Entry<Integer, Integer> entry : freq.entrySet()) {
int num = entry.getKey();
int cnt = entry.getValue();
list[cnt].add(num);
}
int[] ans = new int[k];
int j = 0;
for (int i = maxCnt; j < k; i--) {
for (int x : list[i]) {
ans[j++] = x;
}
}
return ans;
}
}
易错点
- 题目返回的是元素本身,不是出现次数。
Map.Entry中,getKey()是元素,getValue()是频率。List<Integer>[] list = new ArrayList[maxCnt + 1]后,每个桶还需要初始化。- 外层循环应该从
maxCnt往1遍历,不是和k比较频率。 j < k控制的是已经收集的答案数量;简化写法依赖题目“答案唯一”和k有效的保证。
建议测试用例
nums = [1,1,1,2,2,3], k = 2
期望:[1,2]
nums = [1], k = 1
期望:[1]
nums = [1,1,2,2,3], k = 2
期望:[1,2] 或 [2,1]
nums = [4,1,-1,2,-1,2,3], k = 2
期望:[-1,2] 或 [2,-1]
复杂度分析
设数组长度为 n,不同元素个数为 m。
- 统计频率需要
O(n)。 - 建桶需要
O(m)。 - 从高频到低频取答案,最多遍历所有不同元素,时间是
O(m)。
所以总时间复杂度是:
O(n)
桶数组和哈希表需要额外空间:
O(n)
总结
这道题的关键不是“怎么排序频率”,而是“如何根据频率找到对应的元素”。
HashMap 负责建立元素和频率的关系,桶排序负责把相同频率的元素归类。最后从高频桶往低频桶收集元素,就能得到前 k 个高频元素。
下次再写这题时,可以先问自己:
- 我最后放进答案的是
key还是value? - 桶数组里的每个
ArrayList初始化了吗? - 外层循环控制的是频率,还是答案数量?
- 为什么题目“答案唯一”的保证能让收集答案的循环省略内层判满?

1160

被折叠的 条评论
为什么被折叠?



