前 K 个高频元素 图解题解
找出现最多的前 K 个数,堆排序够用,但桶排序能让时间复杂度直接打到 O(n)。
想象统计一篇文章里每个词出现了几次,词最多能出现 n 次(n 是文章总词数)。先开 n+1 个桶,编号 0 到 n,每个词按出现次数直接扔进对应编号的桶。桶建好后从高编号往回数,数够 k 个词就停——既没排序,也没额外开销。桶的数量跟数组长度走,不是固定的,是 n+1。
这道题到底在问什么
- nums
- [1, 1, 1, 2, 2, 3]
- k
- 2
- 输出
- [1, 2]
最优解:一步一步想明白
- 3最直接的想法:用哈希表数出每个数的次数,再把所有数按次数从大到小排序,取前 k 个。能对,但我们只要前 k 个,却把每个数都排得明明白白,多花了 log n 的排序,浪费。
- 4转折:一个数最多出现 n 次,所以「次数」的取值只有 0~n 这有限几种。开一排桶,桶下标 = 出现次数,把每个数丢进「它次数对应的桶」。再从下标最大(次数最高)的桶往回扫,凑够 k 个就停——这样避开了对全部元素的整体排序,整体只 O(n)。
- 5cnt = {1:3, 2:1} …还在扫第一步先扫一遍数组数次数:见一个加一个,哈希表查改都是 O(1)。扫到这里,前三个 1 已让 cnt[1]=3,刚遇到第一个 2,cnt[2]=1。
- 6cnt = {1:3, 2:2, 3:1}数组扫完,得到每个数的次数:1→3、2→2、3→1。计数阶段就这么直白——纯 O(n) 累加。这张表就是下一步装桶的原料。
- 7buckets[3] = [1]第二步装桶。注意画面下半区已经从哈希表换成了「桶阵列」——桶下标就是出现次数。先把 1 丢进它次数对应的「下标3的桶」。2、3 还没放。
- 8buckets[3]=[1] buckets[2]=[2] buckets[1]=[3]接着把 2 丢进「下标2的桶」、3 丢进「下标1的桶」。装桶完成。桶是按次数分的,次数再大也超不过 n,所以桶的数量有限——这正是能用桶排序的根。
- 9res = [1],还差 1 个第三步从下标最大(次数最高)的桶往回扫。先看「下标3的桶」,里面是 1,收走,res=[1],已有 1 个,还差 1 个凑够 k=2。
- 10res = [1, 2] = k 个 → 停倒到「下标2的桶」,取出 2,res=[1,2],凑够 k=2 个,立刻停。看「下标1的桶」标了 ✗未碰——低频的 3 压根没被扫到。这就是比整体排序省的地方:整体排序要把 3 也排进去,桶法直接跳过。答案 [1, 2]。
- 13Top K 高频的通杀思路:先哈希计数,再拿「次数」当桶下标。只要某个量(次数、分数、年龄)取值范围有限,桶排序就能避开 O(n log n) 的整体排序。
- 18把这题练到能复述后,再去同类题里迁移:先复用状态定义,再调整边界条件。
⚠️ 容易写错的地方
✗ 错:直接对所有数按频率 sorted 再切前 k
✓ 对:用桶(次数当下标)或大小为 k 的小顶堆
只要前 k 个却把全部 n 个排了序,白搭 O(n log n);桶排序 O(n)、堆 O(n log k) 都更省
✗ 错:桶数组开 len(nums) 这么长
✓ 对:开 len(nums) + 1,因为次数最大可达 n
一个数最多出现 n 次,要能放下下标 n,桶长必须是 n+1,否则 buckets[n] 越界
✗ 错:从下标 0 的桶开始顺着扫
✓ 对:从最大下标往 1 倒着扫
要的是「高频」,必须先取次数大的桶;顺扫会先拿到低频元素,答案全错
完整代码(Python / C++ / Java)
Python
class Solution:
def topKFrequent(self, nums, k):
count = {}
for x in nums:
count[x] = count.get(x, 0) + 1
buckets = [[] for _ in range(len(nums) + 1)]
for x, c in count.items():
buckets[c].append(x)
ans = []
for freq in range(len(buckets) - 1, 0, -1):
for x in buckets[freq]:
ans.append(x)
if len(ans) == k:
return ans
return ansC++
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;
for (int x : nums) cnt[x]++;
int n = nums.size();
vector<vector<int>> buckets(n + 1);
for (auto& [x, c] : cnt) buckets[c].push_back(x);
vector<int> ans;
for (int f = n; f > 0; f--)
for (int x : buckets[f]) {
ans.push_back(x);
if ((int)ans.size() == k) return ans;
}
return ans;
}
};Java
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> cnt = new HashMap<>();
for (int x : nums) cnt.merge(x, 1, Integer::sum);
List<Integer>[] buckets = new List[nums.length + 1];
for (Map.Entry<Integer,Integer> e : cnt.entrySet()) {
int c = e.getValue();
if (buckets[c] == null) buckets[c] = new ArrayList<>();
buckets[c].add(e.getKey());
}
int[] ans = new int[k]; int idx = 0;
for (int f = nums.length; f > 0 && idx < k; f--)
if (buckets[f] != null)
for (int x : buckets[f]) { ans[idx++] = x; if (idx == k) return ans; }
return ans;
}
}套路模板 · 大小为 k 的小顶堆(背下来)
import heapq
from collections import Counter
cnt = Counter(nums) # 数次数,同桶法第一步
heap = [] # 大小<=k 的小顶堆,存 (次数, 数)
for x, c in cnt.items():
heapq.heappush(heap, (c, x))
if len(heap) > k: # 超过 k 个就弹掉堆顶(当前最低频)
heapq.heappop(heap)
return [x for c, x in heap] # 堆里剩下的就是前 k 高频,O(n log k)class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;
for (int x : nums) cnt[x]++; // 数次数
// 小顶堆:pair<次数, 数>,按次数从小到大,堆顶=当前最低频
priority_queue<pair<int,int>, vector<pair<int,int>>,
greater<>> heap;
for (auto& [x, c] : cnt) {
heap.push({c, x});
if ((int)heap.size() > k) heap.pop(); // 超 k 弹堆顶
}
vector<int> ans;
while (!heap.empty()) { ans.push_back(heap.top().second); heap.pop(); }
return ans; // O(n log k)
}
};class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> cnt = new HashMap<>();
for (int x : nums) cnt.merge(x, 1, Integer::sum); // 数次数
// 小顶堆:按次数升序,堆顶=当前最低频;只留 k 个
PriorityQueue<int[]> heap =
new PriorityQueue<>((a, b) -> a[1] - b[1]); // a=[数, 次数]
for (Map.Entry<Integer,Integer> e : cnt.entrySet()) {
heap.offer(new int[]{e.getKey(), e.getValue()});
if (heap.size() > k) heap.poll(); // 超 k 弹堆顶
}
int[] ans = new int[k];
for (int i = 0; i < k; i++) ans[i] = heap.poll()[0];
return ans; // O(n log k)
}
}复杂度
时间复杂度
O(n)
计数 O(n) + 装桶 O(不同元素数) + 倒扫桶 O(n),都是线性,没有 log n 的排序
空间复杂度
O(n)
哈希表存至多 n 个不同的数 + 长为 n+1 的桶数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 前 K 个高频元素 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
堆和桶排序怎么选?+
两法的主空间都被哈希表 O(去重数) 占住,差别在额外开销:桶法额外要一排长 n+1 的桶数组,堆法只额外占 O(k)。所以数据量大、k 很小时堆更省内存;但桶法 O(n) 时间更稳(堆是 O(n log k)),面试报「次数当下标、O(n) 不排序」也更亮眼。
为什么是小根堆不是大根堆?+
要留最高频的 K 个,小根堆堆顶是这 K 个里最小的,便于和新元素比较替换。
频次相同怎么办?+
题目一般保证答案唯一或允许任意一组,频次并列时任取。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。