当前:LC347 · 前 K 个高频元素 · 首次出现于 Day 14 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC347 · Top K Frequent Elements · 堆

前 K 高频:小顶堆卡住淘汰线

先数每个数出现几次。堆里只留 K 个,堆顶是频率最低的那个:新来的更勤就把它踢走。也可以按频率分桶,从高往低倒。

哈希表统计频率。维护容量 K 的小顶堆:堆未满就放入;满了且新频率大于堆顶才替换。或把数字放进下标等于频率的桶,从高频率往下取 K 个。堆法时间 O(n log K),桶法 O(n),空间都是 O(n)。

时间 O(n log K)空间 O(n)结论先行 · 全文约 6 节
导读

给你一个整数数组和整数 k,返回出现次数最多的 k 个不同的数。顺序不限。同一个数出现多次,只按「这个值出现了几次」计分,不按下标。

主例 nums = [1, 1, 1, 2, 2, 3],k = 2。1 出现 3 次,2 出现 2 次,3 出现 1 次。前两名是 1 和 2。

把所有不同的数按频率排完全序能做对,但是多排了后半截用不上的名次。本文要回答:为什么堆顶可以当门槛,桶下标为什么能代替排序,以及主例里 3 是在哪一步被挡在门外的。

堆顶是当前第 K 名的门槛

第一直觉是先数次数,再把「数字、次数」两两排序,切前 k 个。主例三个不同的数,排序一下就结束。数组很长、k 却很小的时候,为了只要前 2 名去排一万种数字,后半截比较全是浪费。另一种错觉是不数次数、直接在原数组上找「出现得多的」,没有频率表就无法比较 1 和 2 谁更勤。

缺的不是完整排行榜,而是一个能动态维护「当前前 K 名」的小结构。先用哈希表走一遍数组,得到每个值的频率——主例是 1→3、2→2、3→1。接下来只在这张短表上干活。我们要的是频率最高的 K 个,等价于:手里始终拿着 K 个候选人,新来的若比最弱的那个还弱,直接丢掉。

小顶堆按频率比大小,堆顶是这 K 个里最弱的,也就是淘汰线。堆没满,谁来都收。堆满了,新频率 ≤ 堆顶,进不去;新频率更大,弹出堆顶、把新人放进去。扫描结束,堆里剩下的就是前 K 名。每个不同的数只对堆操作一次,代价是 O(log K),不是 O(log n)。

用手走主例,k=2。先把 1 和 2 放进堆:频率 3 和 2,堆顶是更弱的 2,演示第一帧堆里是 [2,1]。轮到 3,频率只有 1,比堆顶门槛 2 小,不换——演示第二帧把这次比较标出来。收尾堆里仍是 1 和 2。3 从未成为前两名。

也可以不用堆。频率最大不会超过 n,开 n+1 个桶,下标就是次数,把数字丢进对应桶里。主例桶 3 里有 1,桶 2 里有 2,桶 1 里有 3;从下标 n 往下扫,收满 k 个停。这一趟是线性的。两种做法第一步都是哈希计数,分歧只在第二步怎么取出前 K。

题目保证答案唯一,不用处理「并列第 K」要不要多返回。空数组或 k 等于不同数字个数时,堆会自然装下全部。不要把原数组排序当频率——排序只能让相同值挨在一起,还得再扫一遍数次数,最后仍要选出前 K,不如一开始就用哈希表。

为什么不排序完整排序付出 O(U log U),U 是不同数字个数。小顶堆只对 K 个元素维持秩序,门槛以下的数看一眼就丢。桶把频率当成下标,连比较都省了。主例 U=3、k=2,三种都能做对,差在规模变大以后。
nums=[1,1,1,2,2,3], k=2
111223
1×32×23×1
堆 (k=2)
2213
堆 [2,1]:频率 2、3

Go:容器 heap + 小顶堆

solution.goGo
func topKFrequent(nums []int, k int) []int {
freq := map[int]int{}
for _, n := range nums { freq[n]++ }
h := &IntHeap{}
for key, c := range freq {
heap.Push(h, item{key, c})
if h.Len() > k { heap.Pop(h) }
}
res := make([]int, 0, k)
for h.Len() > 0 {
res = append(res, heap.Pop(h).(item).val)
}
return res
}

1第一趟只数次数。后面的堆操作次数等于不同数字个数,不再扫原数组。

2先压入再判断长度:堆比 k 多一个时,弹出的一定是当前频率最小的。这和「满了才和堆顶比」是同一道门槛。

3主例 3 的频率是 1,压入后立刻被弹出,堆回到 {1,2}。最后从堆里倒出来的顺序不必按频率排序。

总结

先哈希数次数;小顶堆卡 K 名门槛,或按频率分桶从高往低取。主例 {1,2}。

  • 没有频率表就无法比较谁更勤。主例 1、2、3 的次数是 3、2、1。
  • 堆顶是第 K 名的门槛。3 的次数 1 低于 2,进不了容量 2 的堆。
  • 桶的下标就是次数,从大扫到小同样得到前 K,时间线性。
同族题目
LC215数组中的第 K 个最大元素LC451根据字符出现频率排序LC692前 K 个高频单词