堆-前 K 个高频元素

拿到这道题,我第一反应就是:这不就是上一题第 K 大元素的升级版吗?

思考过程

1. 题目拆解

题目要求:给一个整数数组 nums 和一个整数 k,返回数组中出现频率前 k 高 的元素。比如示例 1:nums = [1,1,1,2,2,3],k=2,1 出现 3 次,2 出现 2 次,3 出现 1 次,所以前 2 高的就是 [1,2]。

2. 我的核心思路

上一题我们是直接对数组元素求第 K 大,这一题的区别在于:我们要比较的不是元素本身,而是元素的出现频率。所以我的思路瞬间就通了:

这一题就是比上一题多记个频率,然后拿频率做堆,统计前 k 个,再去 map 拿数!

拆解成 3 步:

  1. 第一步:统计频率:用哈希表(HashMap)遍历数组,统计每个数字出现的次数。
  2. 第二步:维护小顶堆 :用一个大小为 k 的小顶堆,按照「频率」来排序,只保留频率最高的 k 个元素。
  3. 第三步:提取结果:遍历堆,把里面的元素拿出来,就是我们要的前 k 个高频元素。

3. 为什么还是用小顶堆?

和上一题的逻辑完全一致:

  • 小顶堆的堆顶永远是当前堆里频率最低的元素。
  • 我们维护一个大小为 k 的小顶堆,遍历所有频率:
    • 元素入堆,一旦堆大小超过 k,就把堆顶(频率最低的)删掉。
  • 遍历结束后,堆里剩下的就是频率最高的 k 个元素,完美符合需求。
  • 时间复杂度 O(n logk),比全量排序 O(n logn) 高效很多,空间复杂度 O(n)(哈希表存储频率)。

完整代码实现

java 复制代码
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        // 1. 第一步:用HashMap统计每个数字的出现频率
        Map<Integer, Integer> freqMap = new HashMap<>();
        for (int num : nums) {
            freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
        }

        // 2. 第二步:创建小顶堆,按照「频率」升序排序(堆顶是频率最小的)
        // 小顶堆里存的是数组元素,比较规则是它们对应的频率
        PriorityQueue<Integer> minHeap = new PriorityQueue<>((a, b) -> freqMap.get(a) - freqMap.get(b));

        // 3. 遍历哈希表的所有key,维护堆的大小为k
        for (int num : freqMap.keySet()) {
            minHeap.offer(num);
            // 堆大小超过k,就把堆顶(频率最低的)删掉
            if (minHeap.size() > k) {
                minHeap.poll();
            }
        }

        // 4. 第三步:把堆里的元素拿出来,就是前k个高频元素
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = minHeap.poll();
        }
        return res;
    }
}

代码逐行拆解

1. 统计频率:HashMap 的使用

java 复制代码
Map<Integer, Integer> freqMap = new HashMap<>();
for (int num : nums) {
    freqMap.put(num, freqMap.getOrDefault(num, 0) + 1);
}
  • 我们用 key 存数组中的数字,value 存它出现的次数。
  • getOrDefault(num, 0) 是一个非常实用的写法:如果数字第一次出现,默认次数是 0,然后 + 1;如果已经存在,就拿到当前次数再 + 1,完美避免空指针。

2. 创建小顶堆:自定义比较器

java 复制代码
PriorityQueue<Integer> minHeap = new PriorityQueue<>((a, b) -> freqMap.get(a) - freqMap.get(b));
  • 这里是核心!和上一题直接用默认小顶堆不同,我们需要自定义比较规则 :
    • 比较的不是 a 和 b 本身,而是它们在 freqMap 里的频率。
    • (a, b) -> freqMap.get(a) - freqMap.get(b) 表示:频率小的排在前面,也就是小顶堆,堆顶永远是当前堆里频率最低的元素。

3. 维护堆的大小

java 复制代码
for (int num : freqMap.keySet()) {
    minHeap.offer(num);
    if (minHeap.size() > k) {
        minHeap.poll();
    }
}
  • 遍历哈希表的所有数字,依次入堆。
  • 一旦堆的大小超过 k,就把堆顶(频率最低的)删掉,保证堆里永远只存频率最高的 k 个元素。

4. 提取结果

java 复制代码
int[] res = new int[k];
for (int i = 0; i < k; i++) {
    res[i] = minHeap.poll();
}
  • 遍历堆,把里面的元素依次弹出,存入结果数组。
  • 因为是小顶堆,弹出的顺序是「频率从低到高」,但题目要求可以按任意顺序返回,所以完全没问题。

用示例走一遍流程

我们用示例 1:nums = [1,1,1,2,2,3],k=2 来走一遍完整流程。

1. 统计频率

freqMap 最终结果:{1:3, 2:2, 3:1}

2. 维护小顶堆

  • 入堆 1 → 堆:[1](大小 1 ≤2,不删)
  • 入堆 2 → 堆:[2,1](频率 2 < 3,堆顶是 2,大小 2 ≤2,不删)
  • 入堆 3 → 堆:[3,1,2](频率 1 <2 < 3,堆顶是 3,大小 3>2,删堆顶 3)→ 堆:[2,1]

3. 提取结果

堆里剩下 [2,1],依次弹出:2、1,结果数组 [2,1](题目允许任意顺序,和示例的[1,2]等价)✅

我的做题心得

这道题本质上就是 第 K 大元素的变形题,核心逻辑完全一致,只是把比较元素大小换成了比较元素频率」。

我的核心思考就是一句话:

这一题就是比上一题多记个频率,然后拿频率做堆,统计前 k 个,再去 map 拿数!

只要你吃透了上一题的小顶堆思路,这道题就是手到擒来。这类 TopK 问题的通用解法就是:

  1. 统计你需要比较的指标(频率、大小等)
  2. 用大小为 k 的小顶堆维护前 k 大的元素
  3. 提取结果
相关推荐
启观川9 小时前
数据结构与算法 -第 3 章 常用算法-动态规划
数据结构·笔记·python·算法
码爸10 小时前
排序算法介绍
python·算法·排序算法
动词ing11 小时前
【题目练习】算法 组合总和||
算法
longlongzihan11 小时前
链表找环:Floyd 判圈算法(LeetCode 141 & 142 )
c++·算法·leetcode·链表
专业程序开发源11 小时前
flask家电故障预测系统92491-计算机课程设计/毕业设计
vscode·python·sql·算法·flask·课程设计
Logic10111 小时前
C语言/数据结构欧几里得算法题解:长方形切割最大正方形——贪心划分计数
c语言·数据结构·算法·贪心算法·时间复杂度·空间复杂度·欧几里得算法
yi01111 小时前
LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口
人工智能·笔记·python·算法·leetcode·滑动窗口
极光通讯12 小时前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
淡海水12 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
ClickHouseDB12 小时前
ClickHouse 大规模 OpenTelemetry 数据接入架构的可靠性演进
算法