问题描述
给你一个整数数组 nums 和一个整数 k,请你返回其中出现频率前 k 高的元素。请按升序排列。
- 1 <= nums.length <= 10^5
- k 的取值范围是 [1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
你所设计算法的时间复杂度必须优于 O(n log n),其中 n 是数组大小。
测试样例
样例1:
输入:
nums = [1, 1, 1, 2, 2, 3], k = 2
输出:[1,2]
样例2:
输入:
nums = [1], k = 1
输出:[1]
样例3:
输入:
nums = [4, 4, 4, 2, 2, 2, 3, 3, 1], k = 2
输出:[2,4]
初步思考
对于刚看到题目,最直接的想法可能是暴力求解:
- 统计每个元素的出现次数:遍历数组,记录每个数字出现的次数。
- 排序:按照出现次数对元素进行排序。
- 提取前
k个元素:取出出现次数最高的前k个元素。
问题:这种方法需要对元素进行排序,排序的时间复杂度是 O(n log n),而题目要求时间复杂度必须优于 O(n log n)。 为了满足时间复杂度要求,我们需要寻找更高效的方法。
可行方案
方法一:哈希表 + 桶排序
核心思想:
- 使用哈希表统计频率。
- 利用桶排序的思想,将元素按照频率分配到不同的“桶”中。
- 从高频到低频收集前
k个元素。 - 对结果进行升序排序。
详细步骤:
步骤1:统计频率
- 哈希表(字典) : 哈希表是一种数据结构,能够在常数时间内完成插入、删除和查找操作。
在 Python 中,哈希表通常通过
dict或collections.Counter实现。
在本题中,用于统计每个元素的出现次数。
例如,`nums = [1, 1, 1, 2, 2, 3]`,
统计结果为 `{1: 3, 2: 2, 3: 1}`。
步骤2:创建频率桶
-
桶排序:桶排序是一种线性时间复杂度的排序算法,通过将元素分配到不同的“桶”中,然后分别对每个桶进行排序,最后合并所有桶中的元素。
在本题中,使用桶排序的思想将元素按照出现频率分配到不同的桶中。每个桶的索引代表频率,桶中的元素是具有该频率的数字。这样,频率高的元素会被放在高索引的桶中,便于快速收集前
k个高频元素。例如,最高频率为 3,那么需要创建一个长度为 `4`(索引 `0` 到 `3`)的列表: freq_buckets = [[], [], [], []]。 遍历哈希表,将元素放入对应的桶中: - 频率为 3 的元素 `1` 放入 `freq_buckets[3]`。 - 频率为 2 的元素 `2` 放入 `freq_buckets[2]`。 - 频率为 1 的元素 `3` 放入 `freq_buckets[1]`。 结果:`freq_buckets = [[], [3], [2], [1]]`。
步骤3:收集前 k 个高频元素
-
从高频到低频遍历桶:
从最高频率开始,依次向下遍历频率桶。
将桶中的元素添加到结果列表中,直到收集到
k个元素。例如,`k = 2`: 从 `freq = 3` 开始,收集元素 `1`。 接着从 `freq = 2`,收集元素 `2`。 已收集到 `k = 2` 个元素,停止。
步骤4:对结果进行升序排序
- 排序结果:
收集到的元素可能未按升序排列,因此需要对结果进行排序。
例如,收集到的结果为 `[1, 2]`,排序后仍为 `[1, 2]`。
优点:
1.时间复杂度为 O(n),满足题目要求。
2.利用桶排序的思想,高效地将元素按频率分组。
缺点:
需要额外的空间来存储频率桶。
详细代码如下:
def solution(nums, k):
from collections import Counter
count = Counter(nums) # 统计每个数字的出现次数
max_freq = max(count.values()) # 找到最高频率
freq_buckets = [[] for _ in range(max_freq + 1)] # 创建频率桶
for num, freq in count.items():
freq_buckets[freq].append(num) # 将数字放入对应频率的桶中
res = []
# 从高频到低频遍历频率桶
for freq in range(len(freq_buckets) - 1, 0, -1):
for num in freq_buckets[freq]:
res.append(num)
if len(res) == k:
break
if len(res) == k:
break
res.sort() # 对结果进行升序排序
return res # 返回结果列表
方法二:基于排序的优先队列
优先队列 :在计算机科学中是一种特殊的队列数据结构,其中每个元素都有一个优先级。元素按照优先级的顺序出队。最常见的实现方式是堆,其中最小堆和最大堆是两种主要形式。
在本问题中,我们将使用最小堆来维护前 k 个高频元素。最小堆的特性使得堆顶始终是频率最低的元素,便于在遍历过程中保持堆的大小不超过 k。
为什么选择优先队列?
使用优先队列(尤其是最小堆)可以高效地维护前 k 个高频元素,尤其适用于大规模数据集和 k 相对较小的情况。相比于桶排序,最小堆在某些情况下具有更好的性能和灵活性。
详细步骤
步骤1:统计元素频率
首先,我们需要统计数组中每个元素的出现次数。为此,我们使用哈希表(Hash Table) ,在 Python 中可以利用 collections.Counter 轻松实现。
from collections import Counter
count = Counter(nums)
示例:
对于 `nums = [1, 1, 1, 2, 2, 3]`,统计结果为:{1: 3, 2: 2, 3: 1}
步骤2:构建最小堆
接下来,我们使用一个最小堆来维护前 k 个高频元素。最小堆的特性确保堆顶始终是频率最低的元素,当堆的大小超过 k 时,我们可以迅速移除堆顶元素,确保堆中只保留前 k 个高频元素。
在 Python 中,可以使用 heapq 模块来实现最小堆。
import heapq
heap = []
步骤详解:
-
遍历频率哈希表: 对于每个元素及其频率,执行以下操作:
①插入堆:将元素及其频率作为元组
(frequency, element)插入堆中。②维护堆大小:如果堆的大小超过
k,则移除堆顶元素(频率最低的元素)。 -
堆的大小限制: 通过始终保持堆的大小不超过
k,我们确保堆中只包含前k个高频元素。
示例操作:
对于 nums = [1, 1, 1, 2, 2, 3] 和 k = 2:
插入 (3, 1) → 堆:[(3, 1)]
插入 (2, 2) → 堆:[(2, 2), (3, 1)]
插入 (1, 3) → 堆:[(1, 3), (3, 1), (2, 2)]
堆大小超过 `k = 2`,移除 (1, 3) → 堆:[(2, 2), (3, 1)]
最终堆中保留了 (2, 2) 和 (3, 1),即元素 `2` 和 `1`。
步骤3:提取并排序结果
一旦我们遍历完所有元素并构建好堆,就可以从堆中提取出前 k 个高频元素。由于堆中元素的顺序并不一定按升序排列,我们需要对提取的结果进行升序排序。
步骤详解:
- 提取堆中的元素: 将堆中的所有元素提取到一个列表中。
- 排序: 对提取出来的元素按照数值升序排列。
- 格式化输出: 根据题目要求,将结果转换为逗号分隔的字符串。
示例操作:
堆中元素:[(2, 2), (3, 1)]
提取元素:[2, 1]
升序排序:[1, 2]
格式化输出:"1,2"
优点
- 时间效率高:
- 频率统计:O(n)
- 构建堆:O(n log k)
- 提取和排序:O(k log k)
- 总时间复杂度:O(n log k + k log k) ≈ O(n log k)
-
适用于大规模数据:
当数组规模较大且
k相对较小时,最小堆方法表现尤为出色。 -
空间效率合理:
仅需额外的 O(k) 空间来维护堆,适用于需要控制空间消耗的场景。
缺点
-
实现复杂度较高:
相较于桶排序,使用堆需要更深入的理解和实现技巧。
-
当
k接近n时效率下降:由于堆的操作涉及 O(log k) 的时间复杂度,当
k接近n时,总时间复杂度趋近于 O(n log n),不如桶排序高效。
详细代码如下:
from collections import Counter
def solution(nums, k):
# 步骤1:统计频率
count = Counter(nums)
# 步骤2:构建最小堆
heap = []
for num, freq in count.items():
heapq.heappush(heap, (freq, num))
if len(heap) > k:
heapq.heappop(heap) # 移除频率最低的元素
# 步骤3:提取并排序结果
res = [num for freq, num in heap]
res.sort()
return res