LeetCode 347:前 K 个高频元素,用桶排序做到 O(n)
七崽爱吃小饼干2026/10/19阅读 0
题目
给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。
示例:
text
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
解释:
1出现了 3 次2出现了 2 次3出现了 1 次
频率最高的两个元素是 1 和 2。
约束:
1 <= nums.length <= 10^5k的取值范围是[1, 数组中不同元素的个数]- 题目保证答案唯一
思路
这题可以拆成两步:
- 统计每个元素出现的频率。
- 按频率从高到低,取出前
k个元素。
统计频率用哈希表,时间复杂度是 O(n)。
难点在第二步。常见做法是排序或使用堆:
- 排序:把元素按频率排序,时间复杂度
O(n log n)。 - 最小堆:维护大小为
k的堆,时间复杂度O(n log k)。
这题还可以做到 O(n),因为频率有一个天然的上界:一个元素最多出现 nums.length 次。
于是可以创建一个桶数组:
text
buckets[count] = 所有出现 count 次的元素
桶的下标就是频率,不需要比较大小。统计完成后,从最大的下标开始往前遍历,依次取出元素,直到收集到 k 个。
代码
js
function topKFrequent(nums, k) {
const freq = new Map();
for (const num of nums) {
freq.set(num, (freq.get(num) || 0) + 1);
}
const buckets = Array.from({ length: nums.length + 1 }, () => []);
for (const [num, count] of freq) {
buckets[count].push(num);
}
const result = [];
for (let count = nums.length; count > 0 && result.length < k; count--) {
for (const num of buckets[count]) {
result.push(num);
if (result.length === k) {
return result;
}
}
}
return result;
}
调用示例:
js
console.log(topKFrequent([1, 1, 1, 2, 2, 3], 2));
// [1, 2]
执行过程
以 nums = [1,1,1,2,2,3],k = 2 为例。
第一步,统计频率:
text
1 -> 3
2 -> 2
3 -> 1
第二步,把元素放进对应频率的桶:
text
buckets[3] = [1]
buckets[2] = [2]
buckets[1] = [3]
第三步,从高频往低频遍历:
text
count = 3,取出 1
count = 2,取出 2
已经收集到 2 个元素,返回 [1, 2]。
复杂度
时间复杂度:O(n)
- 遍历数组统计频率:
O(n) - 遍历哈希表放入桶:
O(n) - 从高频到低频遍历桶:最多
O(n)
空间复杂度:O(n)
- 哈希表最多存
n个不同元素 - 桶数组长度为
n + 1 - 结果数组最多存
k个元素
桶和堆的区别
这里的桶不是堆。
桶是一个数组,下标表示频率,buckets[count] 存放所有出现 count 次的元素。它利用频率范围固定在 1..n,直接按下标定位。
堆通常指优先队列,用完全二叉树维护最值,适合动态维护前 k 个元素。用最小堆解这道题的时间复杂度是 O(n log k)。
两者都能解决 Top K 问题,但桶排序在这个场景下少了对频率的比较和调整,因此可以做到 O(n)。
最后
这道题的核心不是排序,而是利用频率的取值范围做计数。
记住三个步骤:
- 用
Map统计频率。 - 用频率作为下标,把元素放进桶里。
- 从高频桶往低频桶取,凑够
k个就返回。
当题目中的值域或频率范围有限时,桶排序往往能把 O(n log n) 降到 O(n)。