Article / 文章

LeetCode 第347题:前K个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。

📖 文章摘要

本文详细解析LeetCode第347题“前K个高频元素”,这是一道中等难度的哈希表和堆题目。文章提供了基于优先队列和桶排序两种解法,包含C#、Python、C++三种语言实现,配有详细的算法分析和性能对比。适合想要学习哈希表和堆应用的程序员。

核心知识点: 哈希表、优先队列、桶排序
难度等级: 中等
推荐人群: 具有基础数据结构知识,想要提升算法应用能力的开发者

题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按任意顺序返回答案。

示例

示例 1:

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

示例 2:

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

提示

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

解题思路

方法一:优先队列

使用哈希表统计频率,然后用小顶堆维护前K个高频元素。

关键点:

  • 使用哈希表统计每个元素出现的频率
  • 使用小顶堆维护K个最大值
  • 当堆的大小超过K时,移除堆顶元素
  • 最后将堆中元素转换为结果数组

具体步骤:

  1. 遍历数组,使用哈希表统计频率
  2. 创建小顶堆,遍历哈希表:
    • 将元素加入堆中
    • 当堆大小超过K时,移除堆顶
  3. 将堆中元素转换为结果数组

时间复杂度:O(nlogk) 空间复杂度:O(n)

方法二:桶排序

使用桶排序,每个桶存储出现频率相同的元素。

关键点:

  • 使用哈希表统计频率
  • 创建频率桶数组
  • 从后向前遍历桶,收集前K个元素
  • 避免使用排序,提高效率

具体步骤:

  1. 统计频率
  2. 创建桶数组
  3. 将元素放入对应频率的桶中
  4. 从高频率向低频率遍历,收集结果

时间复杂度:O(n) 空间复杂度:O(n)

图解思路

优先队列过程分析表

步骤 操作 堆内容 说明
初始 统计频率 {} {1:3, 2:2, 3:1}
第1步 加入1 {(1,3)} 频率为3
第2步 加入2 {(2,2),(1,3)} 频率为2
第3步 加入3 {(3,1),(1,3)} k=2,移除3

桶排序示意图

频率统计:{1:3, 2:2, 3:1}
桶数组:
频率3:[1]
频率2:[2]
频率1:[3]
从高到低收集:[1,2]

代码实现

C# 实现

public class Solution {
    // 方法一:优先队列
    public int[] TopKFrequent(int[] nums, int k) {
        // 统计频率
        Dictionary<int, int> freq = new Dictionary<int, int>();
        foreach (int num in nums) {
            if (!freq.ContainsKey(num)) {
                freq[num] = 0;
            }
            freq[num]++;
        }
        
        // 使用优先队列(小顶堆)
        var pq = new PriorityQueue<int, int>();
        foreach (var pair in freq) {
            pq.Enqueue(pair.Key, pair.Value);
            if (pq.Count > k) {
                pq.Dequeue();
            }
        }
        
        // 构建结果
        int[] result = new int[k];
        for (int i = k - 1; i >= 0; i--) {
            result[i] = pq.Dequeue();
        }
        
        return result;
    }
    
    // 方法二:桶排序
    public int[] TopKFrequentBucket(int[] nums, int k) {
        // 统计频率
        Dictionary<int, int> freq = new Dictionary<int, int>();
        foreach (int num in nums) {
            if (!freq.ContainsKey(num)) {
                freq[num] = 0;
            }
            freq[num]++;
        }
        
        // 创建桶
        List<int>[] buckets = new List<int>[nums.Length + 1];
        foreach (var pair in freq) {
            if (buckets[pair.Value] == null) {
                buckets[pair.Value] = new List<int>();
            }
            buckets[pair.Value].Add(pair.Key);
        }
        
        // 收集结果
        List<int> result = new List<int>();
        for (int i = buckets.Length - 1; i >= 0 && result.Count < k; i--) {
            if (buckets[i] != null) {
                result.AddRange(buckets[i]);
            }
        }
        
        return result.Take(k).ToArray();
    }
}

Python 实现

from collections import Counter
import heapq

class Solution:
    # 方法一:优先队列
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # 统计频率
        count = Counter(nums)
        
        # 使用堆
        return heapq.nlargest(k, count.keys(), key=count.get)
    
    # 方法二:桶排序
    def topKFrequentBucket(self, nums: List[int], k: int) -> List[int]:
        # 统计频率
        count = Counter(nums)
        
        # 创建桶
        bucket = [[] for _ in range(len(nums) + 1)]
        for num, freq in count.items():
            bucket[freq].append(num)
        
        # 收集结果
        result = []
        for i in range(len(bucket) - 1, -1, -1):
            result.extend(bucket[i])
            if len(result) >= k:
                return result[:k]
        
        return result

C++ 实现

class Solution {
public:
    // 方法一:优先队列
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 统计频率
        unordered_map<int, int> freq;
        for (int num : nums) {
            freq[num]++;
        }
        
        // 使用优先队列(小顶堆)
        priority_queue<pair<int, int>, vector<pair<int, int>>, 
            greater<pair<int, int>>> pq;
            
        for (auto& [num, count] : freq) {
            pq.push({count, num});
            if (pq.size() > k) {
                pq.pop();
            }
        }
        
        // 构建结果
        vector<int> result;
        while (!pq.empty()) {
            result.push_back(pq.top().second);
            pq.pop();
        }
        
        reverse(result.begin(), result.end());
        return result;
    }
    
    // 方法二:桶排序
    vector<int> topKFrequentBucket(vector<int>& nums, int k) {
        // 统计频率
        unordered_map<int, int> freq;
        for (int num : nums) {
            freq[num]++;
        }
        
        // 创建桶
        vector<vector<int>> buckets(nums.size() + 1);
        for (auto& [num, count] : freq) {
            buckets[count].push_back(num);
        }
        
        // 收集结果
        vector<int> result;
        for (int i = buckets.size() - 1; i >= 0 && result.size() < k; i--) {
            for (int num : buckets[i]) {
                result.push_back(num);
                if (result.size() == k) {
                    break;
                }
            }
        }
        
        return result;
    }
};

执行结果

C# 实现

  • 执行用时:168 ms
  • 内存消耗:45.2 MB

Python 实现

  • 执行用时:96 ms
  • 内存消耗:19.8 MB

C++ 实现

  • 执行用时:12 ms
  • 内存消耗:13.5 MB

性能对比

语言 执行用时 内存消耗 特点
C# 168 ms 45.2 MB 代码结构清晰
Python 96 ms 19.8 MB 实现简洁
C++ 12 ms 13.5 MB 性能最优

代码亮点

  1. 🎯 多种解法思路清晰
  2. 💡 优先队列实现高效
  3. 🔍 桶排序避免排序
  4. 🎨 代码结构优雅

常见错误分析

  1. 🚫 忘记处理频率相同的元素
  2. 🚫 优先队列使用错误
  3. 🚫 桶排序边界处理不当
  4. 🚫 结果数组大小控制错误

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
优先队列 O(nlogk) O(n) 适合处理流数据 常数较大
桶排序 O(n) O(n) 性能最优 需要额外空间

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新至第347题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!