题目解析
本题的目标是从一个整数数组中找到出现频率前 k 高的元素,并按升序排列返回。题目要求算法的时间复杂度必须优于 O(n log n),其中 n 是数组大小。这意味着我们不能简单地使用排序方法,因为排序的时间复杂度是 O(n log n),而需要考虑一种更高效的方法来解决此问题。
思路
-
计数频率:首先,我们需要统计数组中每个数字的出现频率。Python 的
collections.Counter类是实现这一功能的一个方便工具,它可以迅速计算出每个元素的频率。 -
选择前 k 个高频元素:为了找到频率最高的 k 个元素,我们可以使用最小堆(Min-Heap)。最小堆能够有效地保持堆顶始终为堆中最小的元素,因此可以通过堆的特性方便地移除频率最低的元素。我们可以将元素的频率作为堆的关键字,并通过负数来模拟大根堆,从而确保堆中的元素按频率从大到小排列。
-
排序与返回结果:一旦我们从堆中提取出前 k 个高频元素,接下来只需按元素的值升序排列,然后返回格式化的字符串。
代码详解
import heapq
from collections import Counter
def solution(nums, k):
# 统计每个元素出现的频率
count = Counter(nums)
# 使用最小堆,堆的大小为 k,存储的是 (频率, 元素) 元组
# heapq 默认是小根堆,我们需要频率高的排在前面,所以需要用负数来实现大根堆的效果
heap = []
for num, freq in count.items():
heapq.heappush(heap, (freq, num))
if len(heap) > k: # 保证堆的大小始终为 k
heapq.heappop(heap)
# 从堆中提取元素,并按升序排序
result = [num for freq, num in heap]
result.sort() # 按照元素值升序排列
return ",".join(map(str, result))
-
Counter(nums):这一步通过Counter类计算出每个数字在数组中的频率,返回一个字典,键是数组中的元素,值是该元素的出现次数。 -
最小堆操作:
heapq.heappush(heap, (freq, num))将每个数字的频率和该数字本身组成一个元组(freq, num)并推入堆中。- 如果堆的大小超过了 k,那么我们通过
heapq.heappop(heap)弹出堆顶元素,保证堆的大小始终为 k。
-
提取并排序:
- 使用列表推导
[num for freq, num in heap]从堆中提取所有的元素。 - 对提取出的元素按大小升序排列。
- 使用列表推导
-
返回结果:将结果转换为以逗号分隔的字符串形式并返回。
知识总结
-
Counter的使用:Counter是 Python 中的一个高效工具,常用于统计元素的频率。通过Counter(nums)可以快速计算出一个字典,其中键是数组中的元素,值是该元素的出现次数。它在本题中的作用非常明显,能简化频率统计的过程。 -
堆的使用:堆(特别是最小堆)在选择前 k 个元素的过程中非常有用。通过将频率较高的元素存入堆并保持堆的大小不超过 k,可以有效地找到频率前 k 的元素。使用堆可以将时间复杂度降到 O(n log k),大大优化了排序方法的性能。
-
时间复杂度分析:
- 频率统计:通过
Counter统计频率的时间复杂度为 O(n),其中 n 是数组的长度。 - 堆操作:对于每个元素,我们将其插入堆中,最小堆操作的时间复杂度为 O(log k),总共会进行 n 次插入操作,因此堆的操作复杂度为 O(n log k)。
- 排序操作:最终的排序操作是对 k 个元素进行排序,时间复杂度为 O(k log k)。
因此,总的时间复杂度为 O(n log k),符合题目要求的优于 O(n log n) 的条件。
- 频率统计:通过
-
重点复习数据结构:在解决本题的过程中,堆和哈希表(
Counter)是两个非常关键的工具。建议深入学习堆的操作原理及应用,尤其是最小堆和最大堆的转换(通过负数实现大根堆)。同时要熟悉 Python 中的collections.Counter,这是一个非常高效且常用的工具,能够帮助我们快速处理频率统计问题。
工具运用
豆包MarsCode AI 提供的自动化刷题功能非常方便,可以根据自己的掌握程度进行选择性练习。例如,在本题中,通过自动化的测试与解析,我能够快速理解堆和 Counter 的用法及其应用场景。此外,错题的反馈和详细解析帮助我避免重复错误,进而提升自己的算法水平。