前 K 个高频元素

算法-前 K 个高频元素

背景

  • 算法–前 K 个高频元素

  • 博主以代码随想录算法公开课进行学习

题目

  • 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。

    示例 1:

    • 输入: nums = [1,1,1,2,2,3], k = 2
    • 输出: [1,2]

    示例 2:

    • 输入: nums = [1], k = 1
    • 输出: [1]

    提示:

    • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
    • 你的算法的时间复杂度必须优于 , n 是数组的大小。
    • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
    • 你可以按任意顺序返回答案

思路

  • **核心逻辑:**前K个高频元素的核心解法是「哈希表统计频率 + 小顶堆筛选高频元素」:
    • 哈希表:统计每个元素的出现频率(O(n));
    • 小顶堆:维护当前「频率前k高」的元素,堆顶是这k个元素中频率最低的,遍历过程中仅保留高频元素,最终堆中即为结果(堆操作的时间复杂度为O(m log k),m是不同元素的个数,m≤n);
    • 优势:时间复杂度为O(n + m log k),优于O(n log n)(直接排序的复杂度),满足题目进阶要求。
  • 具体步骤:
    • 统计频率:遍历数组,用HashMap记录每个元素的出现次数(键=元素,值=频率);
    • 小顶堆筛选:遍历HashMap中的键值对,维护大小为k的小顶堆:
      • 堆大小 < k:直接将「元素+频率」加入堆;
      • 堆大小 = k:若当前元素频率 > 堆顶元素频率 → 弹出堆顶(频率最低的元素),加入当前元素;
    • 提取结果:小顶堆中存储的是频率前k高的元素,但弹出顺序是「频率低→高」,需倒序存入结果数组。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>();
for(int num : nums){
map.put(num,map.getOrDefault(num,0)+1);
}
//定义小顶堆,比较逻辑是pair1[1]-pair2[1],即频率低的排在对头
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1,pair2)-> pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry : map.entrySet()){
if(pq.size()<k){ // 堆大小不足k时,直接加入
pq.add(new int[] {entry.getKey(),entry.getValue()});
}else{
// 当前元素频率 > 堆顶元素频率(堆顶是当前k个元素中频率最低的)
if(entry.getValue()>pq.peek()[1]){
pq.poll();// 弹出频率最低的元素
pq.add(new int[] {entry.getKey(),entry.getValue()}); // 加入当前高频元素
}
}
}
// 倒序存结果:小顶堆弹出的顺序是「频率低→高」,所以从后往前填
int[] ans = new int[k];
for(int i = k-1;i>=0;i--){
ans[i] = pq.poll()[0];
}
return ans;
}
}

总结

  • 核心知识点:
    • 哈希表的应用:HashMap统计频率是处理「元素 - 频次」问题的基础,getOrDefault方法简化了「存在则 + 1,不存在则初始化为 1」的逻辑;
    • 小顶堆的核心价值:
      • 维护 k 个高频元素,堆顶是当前 k 个元素中频率最低的,保证堆中始终是「频率前 k 高」的元素;
      • 堆的插入 / 弹出操作复杂度为 O (log k),相比直接排序(O (m log m)),当 k 远小于 m 时效率更高;
    • 时间复杂度优化:
      • 统计频率:O (n);
      • 堆操作:O (m log k)(m 是不同元素的个数,m≤n);
      • 总复杂度:O (n + m log k),优于 O (n log n),满足题目进阶要求。