← 返回题目列表

前 K 个高频元素如何用堆解决?

高频 中等 第 5 / 28 题 更新于 2026/07/29
TopK高频元素哈希表

简化版

先用哈希表统计每个元素出现次数,再用大小为 K 的小顶堆按频次维护当前频率最高的 K 个元素。堆顶是这 K 个候选里频次最低的元素,新元素频次更高时替换它。

详细版

前 K 个高频元素比普通 Top K 多了一步:比较键不是元素值,而是出现频次。

步骤:

  1. 扫描数组,用 HashMap<元素, 次数> 统计频次。
  2. 遍历 map,把 (元素, 频次) 放入小顶堆。
  3. 堆大小超过 K 时弹出堆顶,也就是当前候选中频次最低的元素。
  4. 遍历结束后,堆里就是前 K 个高频元素。
int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> freq = new HashMap<>();
    for (int x : nums) freq.put(x, freq.getOrDefault(x, 0) + 1);

    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
    for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
        pq.offer(new int[]{e.getKey(), e.getValue()});
        if (pq.size() > k) pq.poll();
    }

    int[] ans = new int[pq.size()];
    for (int i = ans.length - 1; i >= 0; i--) ans[i] = pq.poll()[0];
    return ans;
}

若数组长度为 n,不同元素个数为 m,统计频次 O(n),堆维护 O(m log K),空间 O(m + K)。如果 K 接近 m,桶排序也常被追问。

完整版教学

一、这题为什么不是直接对元素值建堆

题目问的是“出现频率最高”,不是“数值最大”。例如数组 [100, 1, 1, 2, 2, 2],数值最大的 100 只出现 1 次,而高频元素是 2。比较键必须从元素值切换成频次,这就是本题和普通 Top K 的第一层区别。

所以第一步一定是频次统计。没有频次表,堆就不知道该按什么排序。哈希表负责把原始数组压缩成 (元素, 次数),堆负责从这些键值对中选出频次最高的 K 个。

nums = [1,1,1,2,2,3]
freq:
  1 -> 3
  2 -> 2
  3 -> 1

Top 2 高频元素:1, 2

二、为什么用小顶堆维护高频候选

和普通 Top K 一样,求“频次最高的 K 个”时,堆顶应该放候选集合里最弱的元素,也就是频次最低的那个。这样新元素进来时,只要看它是否比堆顶频次高,就能决定是否替换。

目标堆大小比较键堆顶含义
前 K 个高频元素K出现次数当前候选中频次最低
最大 K 个数值K元素值当前候选中数值最低
最小 K 个数值K元素值当前候选中数值最高

如果用大顶堆,也可以把所有 (元素, 频次) 都放进去,再弹 K 次,复杂度是 O(m log m)。大小为 K 的小顶堆只保留候选,复杂度是 O(m log K),当 K 远小于 m 时更合适。

三、带数字走一遍堆变化

设频次表为:A:5, B:2, C:7, D:3, E:6,要求 K=3。小顶堆按频次排序,堆顶是候选里频次最低的。

放 A(5): [A5]
放 B(2): [B2, A5]
放 C(7): [B2, A5, C7]       候选 A,C,B
放 D(3): 先入后 size=4,弹 B2 -> [D3, A5, C7]
放 E(6): 先入后 size=4,弹 D3 -> [A5, E6, C7]

最终 Top 3:A(5), E(6), C(7)

注意最终堆内部不一定按频次降序排列。如果题目要求输出顺序也按频次从高到低,还要把结果再排序或倒序弹出后整理。

四、桶排序为什么也常被拿来比较

频次最大不会超过 n,因此可以建 n + 1 个桶,bucket[f] 存所有出现 f 次的元素,然后从高频桶往低频桶收集 K 个。桶排序时间可以做到 O(n),但需要额外桶数组,且实现上要处理大量空桶。

freq:
  1 -> 3
  2 -> 2
  3 -> 1

bucket[1] = [3]
bucket[2] = [2]
bucket[3] = [1]
从 3 往 1 扫,取到 [1,2]

面试回答可以这样比较:堆法通用、空间与 K 有关、适合大 m 小 K;桶法理论线性、适合频次范围明确且能接受桶空间。两者都建立在哈希统计之上。

五、工程边界:并列频次、输出顺序和大数据

如果多个元素频次相同,题目通常允许任意顺序;如果要求按数值或首次出现顺序打破平局,比较器必须明确写出来。比较器不完整会导致线上结果不稳定,尤其在 Java PriorityQueue 中,相同优先级元素的弹出顺序没有稳定性保证。

PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
    if (a[1] != b[1]) return Integer.compare(a[1], b[1]);
    return Integer.compare(a[0], b[0]); // 平局时按元素值
});

如果数据非常大,单机内存放不下频次表,就要先做分片统计或使用外部存储聚合,再对聚合后的频次做 Top K。堆解决的是候选选择,不负责消除统计阶段的内存压力。

六、常见误区与追问

记忆钩子:这题有两层压缩,先用哈希表把数组压成频次,再用小顶堆把频次压成 K 个候选。

这一节面试官通常会围绕 2 个边界继续追问:比较键到底是不是频次,以及输出顺序是否有额外要求。回答时要先把“统计阶段”和“筛选阶段”拆开,再说明堆顶为什么代表最低频候选,而不是把普通 Top K 模板原样套过来。

  • 误区:直接对原数组元素建堆即可。 原数组没有频次信息,堆无法知道谁出现得多。
  • 误区:堆顶应该是最高频元素。 大小为 K 的小顶堆里,堆顶是当前候选中最低频的淘汰门槛。
  • 误区:最终堆数组就是按频次降序。 堆只保证堆顶,不保证整体排序。
  • 追问:K 接近不同元素个数时堆还划算吗? 优势会变小,可以考虑桶排序或直接排序频次表。
  • 追问:并列频次怎么处理? 看题目要求;无要求可任意,有要求必须写进比较器。
  • 追问:复杂度里的 m 是什么? m 是不同元素个数,堆遍历的是频次表而不是原数组的每个位置。

七、加强记忆

前 K 个高频元素的主线是“先统计,再筛选”。哈希表把每个元素映射到出现次数,大小为 K 的小顶堆把“当前最高频 K 个候选”维护住,堆顶就是候选中最低频的门槛。新频次高于门槛就替换,低于门槛就丢弃。记忆时把它和普通 Top K 对照:普通 Top K 的比较键是元素值,本题的比较键是频次;其他维护候选的思想完全一致。