【堆】LC 347.前 K 个高频元素

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

347.前 K 个高频元素

2、题目描述

二、个人思路整理

1、思路分析

思路1:小顶堆

  • 核心思路:维护一个大小为 k k k 的小顶堆。
    • 遍历哈希表中的每个 (元素, 频次) 对并压入堆中。
    • 当堆内元素个数超过 k k k 时,弹出堆顶元素(即当前堆中频次最低的那个)。
    • 遍历结束后,堆里剩下的 k k k 个元素就是频次最高的前 k k k 个。

思路2:桶排序

  • 核心思路:任何元素的最高频次不会超过数组总长度 n n n。
    • 建立一个大小为 n + 1 n + 1 n+1 的桶数组 buckets,其中索引 i 代表频次,buckets[i] 存放所有出现次数为 i 的元素集合。
    • 从后向前(频次由高到低)遍历桶,依次收集元素,直到凑齐 k k k 个为止。

2、解题代码

思路1:小顶堆

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 1. 统计每个元素出现的频次:key 为元素数值,value 为出现次数
        unordered_map<int, int> count;
        for (int num : nums) {
            count[num]++;
        }

        // 2. 建立小顶堆:pair<频次, 元素值>
        // greater 会按照 pair 的 first(频次)升序排列,即堆顶始终是当前堆中频次最小的元素
        using pii = pair<int, int>;
        priority_queue<pii, vector<pii>, greater<pii>> min_heap;

        for (const auto& [val, freq] : count) {
            min_heap.emplace(freq, val);
            // 保持堆的大小不超过 k,超出时弹出频次最小的顶堆
            if (min_heap.size() > k) {
                min_heap.pop();
            }
        }

        // 3. 收集堆中留存的前 k 个高频元素
        vector<int> result;
        result.reserve(k);
        while (!min_heap.empty()) {
            result.push_back(min_heap.top().second);
            min_heap.pop();
        }
        
        return result;
    }
};

复杂度分析

  • 时间复杂度: O ( n log ⁡ k ) O(n \log k) O(nlogk)。哈希表最多有 n n n 个不同元素,向容量为 k k k 的堆中插入元素耗时 O ( log ⁡ k ) O(\log k) O(logk)。
  • 空间复杂度: O ( n ) O(n) O(n)。用于哈希表与堆。

思路2:小顶堆

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 1. 统计每个元素的出现频次
        unordered_map<int, int> count;
        for (int num : nums) {
            count[num]++;
        }

        // 2. 将元素按频次分桶
        // 频次最多为 nums.size(),因此开辟大小为 n + 1的桶数组
        // buckets[i] 存放所有出现次数恰好为 i 的元素
        int n = nums.size();
        vector<vector<int>> buckets(n + 1);
        for (const auto& [val, freq] : count) {
            buckets[freq].push_back(val);
        }

        // 3. 从大到小倒序遍历桶,贪心收集前 k 个高频元素
        vector<int> result;
        result.reserve(k);
        for (int freq = n; freq > 0 && result.size() < k; freq--) {
            for (int val : buckets[freq]) {
                result.push_back(val);
                if (result.size() == k) {
                    break;
                }
            }
        }

        return result;
    }
};

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n)。遍历数组统计频次、填充桶和逆序收集结果均在线性时间内完成。
  • 空间复杂度: O ( n ) O(n) O(n)。开辟一维数组统计频次。

三、知识风暴

小顶堆 是本题的核心数据结构。它能在 O ( log ⁡ k ) O(\log k) O(logk) 时间内完成插入与弹出,始终维护「当前频次最高的前 k k k 个元素」,是解决「前 K 大/小元素」类问题的经典做法。理解堆的维护过程与边界控制,对掌握本题至关重要。

算法核心思想:

  • 固定容量 :维护一个大小为 k k k 的小顶堆,堆顶始终是堆中频次最低的元素。
  • 淘汰机制 :每压入一个新元素,若堆大小超过 k k k,立即弹出堆顶,保证堆内始终是「已遍历元素中频次最高的前 k k k 个」。
  • 与排序的区别 :完整排序需要 O ( n log ⁡ n ) O(n \log n) O(nlogn);而小顶堆只需 O ( n log ⁡ k ) O(n \log k) O(nlogk),当 k ≪ n k \ll n k≪n 时优势明显,且无需一次性排序全部元素。

常见对比:小顶堆 vs 桶排序

  • 小顶堆 :时间复杂度 O ( n log ⁡ k ) O(n \log k) O(nlogk),空间复杂度 O ( k ) O(k) O(k)。适合数据量极大、无法一次性载入内存的流式场景,或需要多次查询前 K 大元素时。
  • 桶排序 :时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)。适合频次范围已知(不超过数组长度)且内存充足的场景,线性时间即可完成。
  • 共同点:两者都能在不必完整排序的前提下找到前 K 个高频元素。区别在于小顶堆不修改原数组、可应对流式数据;桶排序需要额外开辟与数组长度等价的桶空间。

"小顶堆"设计思想:

  • 核心思想 :利用堆「堆顶最小」的特性,让频次最低的元素始终暴露在堆顶,一旦堆超容就将其弹出,从而让堆内始终沉淀频次最高的 k k k 个元素。
  • 与本题的联系 :本题要求「前 K 个高频元素」,恰好对应堆的「淘汰低频、保留高频」过程。遍历完哈希表后,堆中剩下的 k k k 个元素即为答案。
  • 注意事项 :堆中存储的是 (频次, 元素值) 的二元组,比较器按频次升序排列,才能保证堆顶是频次最低者;弹出时取 second 才是元素值本身。

使用要点:

  • 二元组入堆 :priority_queue 默认按 pair 的 first 升序,因此把频次放第一位、元素值放第二位,即可实现「按频次维护小顶堆」。
  • 容量控制 :每插入一个元素后判断 size() > k,超出即 pop(),保证堆大小始终不超过 k k k。
  • 结果收集 :循环弹出堆顶,取 top().second 存入结果数组,即可得到前 k k k 个高频元素。
  • 复杂度权衡 :当 k k k 接近 n n n 时, O ( n log ⁡ k ) O(n \log k) O(nlogk) 会退化为 O ( n log ⁡ n ) O(n \log n) O(nlogn),此时可考虑改用桶排序获得线性复杂度。

算法变体与扩展:

  • 数组中的第K个最大元素(LeetCode 215) :维护大小为 k k k 的小顶堆,堆顶即为第 K 大元素,是本题的姊妹题。
  • 数据流中的第 K 大元素(LeetCode 703):动态数据流场景,用小顶堆维护前 K 大,是堆解法的进阶应用。
  • 最小的K个数(剑指 Offer 40):改用大顶堆维护最小的 K 个数,思路完全对称。
  • 前 K 个高频单词(LeetCode 692):在本题基础上增加字典序比较,堆的比较器需自定义。

相关 LeetCode 例题:

代码语法点:using pii = pair<int, int>;

  • 作用 :这是 C++11 引入的「类型别名」声明,等价于 typedef pair<int, int> pii;。它给 pair<int, int> 起了一个更简短的名字 pii,后续代码中写 pii 就等同于写 pair<int, int>。
  • 为什么用 :本题的堆中要存储 (频次, 元素值) 二元组,类型是 pair<int, int>。若每次都写全称,代码会显得冗长;用 using 起别名后,priority_queue<pii, vector<pii>, greater<pii>> 读起来更清爽,也便于统一修改类型。
  • 与 typedef 的区别 :using 语法更直观(别名在左、原类型在右),且支持模板别名等更高级的用法,是 C++11 之后更推荐的方式;typedef 是 C 语言时代的旧写法,两者在本题场景下效果完全一致。
  • 本题中的实际含义 :pii 的 first 存频次、second 存元素值。配合 greater<pii> 比较器,堆会按 first(频次)升序排列,从而保证堆顶始终是频次最低的元素,实现「小顶堆」的淘汰逻辑。
相关推荐
H.莓飛1 小时前
【数据结构】栈_OJ题
linux·数据结构·算法·centos
Zootopia6262 小时前
多架 eVTOL集群排班调度方案思考
人工智能·算法·数学建模·matlab·动态规划·无人机·evtol
和裕2 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法
朝朝辞暮i2 小时前
C++ 第 13 课:值传递 —— 为什么函数里改了,外面却没变?
java·c++·算法
淡海水2 小时前
02-02-原理篇-分代GC
算法·unity·c#·游戏引擎·.net·gc
无忧.芙桃3 小时前
C++语言原理与实践(十):vector类的底层实现
开发语言·c++·算法
乐迪信息3 小时前
AI防爆摄像机,监测港口船舶航行偏航隐患
大数据·人工智能·深度学习·算法·计算机视觉
0+1114 小时前
算法 --模拟
c++·算法·leetcode
青山是哪个青山4 小时前
LeetCode 300:最长递增子序列
算法