跳到主要内容

347. 前 K 个高频元素

题目描述​

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

示例 1:

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

示例 2:

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

提示:

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

进阶: 所设计算法的时间复杂度必须优于 O(n log n),其中 n 是数组大小。


解题思路​

第一步:理解问题本质​

这道题的核心是统计频率 + 找出前 k 大。

可以拆解为两个子问题:

  1. 统计每个元素出现的次数
  2. 从所有元素中找出出现次数最多的 k 个

第二步:暴力解法​

思路: 用哈希表统计频率,然后对所有频率排序,取前 k 个。

from typing import List
from collections import Counter

class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计频率
freq_map = Counter(nums)
# 按频率降序排序,取前 k 个
# key 用负频率省掉 reverse=True;x[1] 就是出现次数
sorted_items = sorted(freq_map.items(), key=lambda x: -x[1])
# 切片取前 k 个,只保留元素本身
return [num for num, _ in sorted_items[:k]]

为什么不够好? 排序的时间复杂度是 O(n log n),其中 n 是不同元素的个数。当数据量很大时,效率不够高。

第三步:优化解法 - 最小堆​

关键洞察: 我们只需要前 k 个,不需要对所有元素排序。

思路: 维护一个大小为 k 的最小堆:

  • 堆中保存当前频率最高的 k 个元素
  • 堆顶是这 k 个元素中频率最小的
  • 遇到频率更高的元素时,替换堆顶
import heapq
from collections import Counter

class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
freq_map = Counter(nums)
# 堆里放 (频率, 元素),堆顶是这 k 个里频率最小的那个
heap = []

# 只遍历出现过的元素,而不是原数组
for num, freq in freq_map.items():
if len(heap) < k:
# 还没满 k 个,直接入堆
heapq.heappush(heap, (freq, num))
elif freq > heap[0][0]:
# 比堆顶更频繁就替换堆顶,堆里始终是当前前 k 高
heapq.heapreplace(heap, (freq, num))

# 题目允许任意顺序,直接按堆内顺序取出元素
return [num for _, num in heap]

复杂度: 时间 O(n log k),空间 O(n + k)

第四步:最优解法 - 桶排序​

关键洞察: 频率的范围是 1 到 n(数组长度),可以用桶排序达到 O(n)。

思路:

  1. 统计每个元素的频率
  2. 创建桶数组,下标表示频率,值是该频率的所有元素
  3. 从高频率往低频率收集元素,直到收集够 k 个
from collections import Counter

class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
cnt = Counter(nums)
# 桶下标就是频率,长度要开到最大频率才能直接按下标存取
max_cnt = max(cnt.values())

# 桶排序:下标是频率,值是该频率的元素列表
# 长度取 max_cnt+1,下标才能直接取到 max_cnt
buckets = [[] for _ in range(max_cnt + 1)]
for num, freq in cnt.items():
# 同一频率可能对应多个元素,所以每个桶是列表
buckets[freq].append(num)

# 从高频率往低频率收集
ans = []
for freq in range(max_cnt, 0, -1):
# 同一频率的元素一次全收,可能一次就超过 k 个
ans.extend(buckets[freq])
if len(ans) >= k:
# 已经够 k 个,截断返回,低频的不用再看
return ans[:k]

# 兜底返回,正常流程在前面就已经返回
return ans[:k]

完整代码实现​

算法可视化 · 逐帧回放这道题的代码10 帧 · 录制输入 [1,1,1,2,2,3], 2
from typing import List
from collections import Counter
import heapq

class Solution:
"""
前 K 个高频元素 - 堆排序解法

核心思路:
1. 使用 Counter 统计每个元素的出现频率
2. 维护一个大小为 k 的最小堆
3. 堆顶元素是 k 个高频元素中频率最小的
4. 当遇到频率更高的元素时,替换堆顶元素

时间复杂度: O(n log k)
空间复杂度: O(n + k)
"""
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计每个元素的出现频率
freq_map = Counter(nums)

# 维护一个大小为 k 的最小堆
heap = []
for num, freq in freq_map.items():
if len(heap) < k:
heapq.heappush(heap, (freq, num))
else:
# 堆已满,如果当前频率大于堆顶,替换堆顶
if freq > heap[0][0]:
heapq.heapreplace(heap, (freq, num))

return [num for _, num in heap]

示例推演​

以 nums = [1,1,1,2,2,3], k = 2 为例:

第一步:统计频率

元素: 频率
1: 3
2: 2
3: 1

第二步:维护大小为 2 的最小堆

步骤当前元素频率堆状态操作
113[(3, 1)]堆未满,直接入堆
222[(2, 2), (3, 1)]堆未满,直接入堆
331[(2, 2), (3, 1)]堆已满,1 < 堆顶2,跳过

最终结果: 从堆中提取元素 [1, 2]


复杂度分析​

解法时间复杂度空间复杂度说明
暴力排序O(n log n)O(n)对所有频率排序
最小堆O(n log k)O(n + k)只维护 k 个元素
桶排序O(n)O(n)利用频率范围有限的特点

说明:

  • n 是数组长度
  • 最小堆解法在 k << n 时优势明显
  • 桶排序达到理论最优,但需要额外空间

易错点总结​

1. 最大堆 vs 最小堆的选择​

错误思路: 用最大堆直接取前 k 个。

问题: 最大堆需要将所有元素入堆,空间复杂度 O(n)。

正确做法: 用最小堆维护 k 个元素,空间复杂度 O(k)。

2. heapreplace 与 heappushpop 的区别​

# heapreplace: 先替换堆顶,再调整堆
# 已经确认新元素比堆顶更大,替换堆顶最省事
heapq.heapreplace(heap, (freq, num)) # 推荐

# heappushpop: 先入堆新元素,再弹出堆顶
# 这里已知新元素比堆顶大,两者结果相同,只是多一次堆顶比较
heapq.heappushpop(heap, (freq, num)) # 不推荐,多一次比较

3. Counter 的使用技巧​

# 获取频率最高的 k 个元素(面试时可能被禁止)
# 一行就能写完,但面试通常要求手写堆或桶排序
Counter(nums).most_common(k)

# 遍历频率字典
# items() 直接给出 (元素, 出现次数) 对
for num, freq in freq_map.items():
pass

扩展思考​

1. 为什么桶排序可以达到 O(n)?​

因为频率的范围是确定的(1 到数组长度),属于范围有限的整数排序,适合用桶排序。

2. 如果要求按频率降序输出?​

堆解法的结果是无序的,如果需要有序,可以对结果再排序:

result = [num for _, num in heap]
# 堆内顺序本身是乱的,要按频率降序输出就得再排一次
result.sort(key=lambda x: -freq_map[x])

3. 相关题目​


相关题目​

自测​

加载评论中...