青训营X豆包MarsCode 学习笔记:寻找出现频率前 k 高的元素 | 豆包MarsCode AI刷题

69 阅读8分钟

一、题目背景与分析

题目描述: 给你一个整数数组 nums 和一个整数 k,请你用一个字符串返回其中出现频率前 k 高的元素。请按升序排列。你所设计算法的时间复杂度必须优于 O(n log n),其中 n 是数组大小。

输入

  • nums:一个正整数数组。
  • k:一个整数。

返回

  • 返回一个包含 k 个元素的字符串,数字元素之间用逗号分隔。数字元素按升序排列,表示出现频率最高的 k 个元素。

参数限制

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

示例

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

二、思路分析

  1. 问题建模

    • 问题的核心在于找到数组中出现频率最高的 k 个元素,并按升序排列。
    • 需要一个高效的方法来统计每个元素的频率,并从中筛选出前 k 个频率最高的元素。
  2. 算法选择

    • 使用哈希表(Counter)来统计每个元素的频率。
    • 使用最小堆(heap)来维护频率最高的 k 个元素。最小堆的特性使得我们可以高效地维护前 k 个元素,而不需要对整个数组进行排序。
  3. 具体步骤

    • 使用 Counter 统计每个元素的频率。
    • 使用最小堆来存储频率和元素的负值(因为 Python 的 heapq 默认是最小堆,我们需要最大堆的效果)。
    • 遍历频率表,将每个元素的频率和负值加入堆中,如果堆的大小超过 k,则弹出最小的元素。
    • 从堆中提取元素并按升序排列。
    • 将结果转换为字符串,元素之间用逗号分隔。

三、代码实现

from collections import Counter
import heapq

def solution(nums, k):
    # 统计每个元素的频率
    frequency = Counter(nums)
    
    # 使用堆来获取频率最高的 k 个元素
    # 这里我们使用最小堆,因为我们只需要前 k 个元素
    heap = []
    for num, freq in frequency.items():
        # 将频率和元素的负值(因为我们需要最大堆)放入堆中
        heapq.heappush(heap, (freq, -num))
        # 如果堆的大小超过 k,弹出最小的元素
        if len(heap) > k:
            heapq.heappop(heap)
    
    # 从堆中提取元素并按升序排列
    result = []
    while heap:
        freq, num = heapq.heappop(heap)
        result.append(-num)
    
    # 将结果按升序排列
    result.sort()
    
    # 将结果转换为字符串,元素之间用逗号分隔
    return ','.join(map(str, result))

if __name__ == "__main__":
    # 你可以在这里添加更多的测试用例
    print(solution([1, 1, 1, 2, 2, 3], 2) == "1,2")
    print(solution([1], 1) == "1")

四、知识点总结

  1. 哈希表(Counter)

    • Countercollections 模块中的一个类,用于统计可哈希对象的频率。
    • 使用 Counter 可以方便地统计数组中每个元素的出现次数。
    • 示例:frequency = Counter(nums)
  2. 堆(Heap)

    • 堆是一种特殊的完全二叉树,分为最小堆和最大堆。
    • 最小堆的根节点是堆中最小的元素,最大堆的根节点是堆中最大的元素。
    • Python 的 heapq 模块提供了最小堆的实现,可以通过存储元素的负值来实现最大堆的效果。
    • 堆的主要操作包括:heappush(插入元素)、heappop(弹出最小元素)、heapify(将列表转换为堆)。
    • 示例:heapq.heappush(heap, (freq, -num))
  3. 时间复杂度分析

    • 统计频率的时间复杂度为 O(n),其中 n 是数组的长度。
    • 插入和弹出堆的时间复杂度为 O(log k),因为堆的大小最多为 k。
    • 总体时间复杂度为 O(n log k),优于 O(n log n)。
  4. 字符串处理

    • 使用 mapjoin 方法将列表转换为字符串,元素之间用逗号分隔。
    • 示例:return ','.join(map(str, result))

五、算法优化

1. 优化哈希表的使用

虽然 Counter 已经非常高效,但我们可以通过直接使用字典来减少额外的开销。

2. 优化堆的使用

使用最小堆来维护前 k 个频率最高的元素,但可以通过直接存储频率和元素来减少负值的使用。

3. 减少不必要的操作

在提取堆中的元素时,可以直接构建结果列表,避免多次操作。

4. 使用更高效的数据结构

在某些情况下,可以使用其他数据结构来进一步优化性能。

优化后的代码

from collections import defaultdict
import heapq

def solution(nums, k):
    # 统计每个元素的频率
    frequency = defaultdict(int)
    for num in nums:
        frequency[num] += 1
    
    # 使用堆来获取频率最高的 k 个元素
    heap = []
    for num, freq in frequency.items():
        # 将频率和元素放入堆中
        heapq.heappush(heap, (freq, num))
        # 如果堆的大小超过 k,弹出最小的元素
        if len(heap) > k:
            heapq.heappop(heap)
    
    # 从堆中提取元素并按升序排列
    result = [num for freq, num in sorted(heap, key=lambda x: x[1])]
    
    # 将结果转换为字符串,元素之间用逗号分隔
    return ','.join(map(str, result))

if __name__ == "__main__":
    # 你可以在这里添加更多的测试用例
    print(solution([1, 1, 1, 2, 2, 3], 2) == "1,2")
    print(solution([1], 1) == "1")
    print(solution([4, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2) == "4,3")

优化点详解

  1. 使用 defaultdict 代替 Counter

    • defaultdict 可以直接初始化为 int 类型,默认值为 0,这样可以减少 Counter 的额外开销。
    • 示例:frequency = defaultdict(int)
  2. 直接存储频率和元素

    • 在堆中直接存储频率和元素,避免使用负值。
    • 示例:heapq.heappush(heap, (freq, num))
  3. 减少不必要的操作

    • 在提取堆中的元素时,直接构建结果列表,避免多次操作。
    • 示例:result = [num for freq, num in sorted(heap, key=lambda x: x[1])]
  4. 使用 sorted 进行排序

    • 在提取堆中的元素后,使用 sorted 按元素值进行排序,确保结果按升序排列。
    • 示例:sorted(heap, key=lambda x: x[1])

进一步优化

  1. 使用桶排序

    • 如果数组中元素的频率范围较小,可以使用桶排序来进一步优化。
    • 创建一个频率桶,每个桶存储频率相同的元素,然后从高频率到低频率依次取出前 k 个元素。
  2. 使用快速选择

    • 如果只需要前 k 个元素,可以使用快速选择算法(Quickselect)来找到第 k 大的元素,然后再提取前 k 个元素。
    • 快速选择算法的时间复杂度为 O(n),在某些情况下比堆更高效。

示例:使用桶排序

from collections import defaultdict

def solution(nums, k):
    # 统计每个元素的频率
    frequency = defaultdict(int)
    for num in nums:
        frequency[num] += 1
    
    # 创建频率桶
    max_freq = max(frequency.values())
    buckets = [[] for _ in range(max_freq + 1)]
    for num, freq in frequency.items():
        buckets[freq].append(num)
    
    # 从高频率到低频率依次取出前 k 个元素
    result = []
    for freq in range(max_freq, 0, -1):
        for num in buckets[freq]:
            result.append(num)
            if len(result) == k:
                break
        if len(result) == k:
            break
    
    # 将结果按升序排列
    result.sort()
    
    # 将结果转换为字符串,元素之间用逗号分隔
    return ','.join(map(str, result))

if __name__ == "__main__":
    # 你可以在这里添加更多的测试用例
    print(solution([1, 1, 1, 2, 2, 3], 2) == "1,2")
    print(solution([1], 1) == "1")
    print(solution([4, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 2) == "4,3")

总结

通过上述优化方法,我们可以显著提高算法的效率。使用 defaultdict 代替 Counter 可以减少额外的开销,直接存储频率和元素可以避免使用负值,减少不必要的操作可以提高代码的效率。此外,使用桶排序或快速选择算法可以在某些情况下进一步优化性能。理解这些优化方法,不仅有助于解决当前的问题,还可以在其他类似的统计和排序问题中发挥作用。

六、学习心得

  1. 理解问题的本质

    • 本题的核心在于高效地找到数组中出现频率最高的 k 个元素。通过将问题分解为统计频率和维护前 k 个元素两个部分,可以清晰地理解问题的本质。
    • 使用哈希表和堆可以有效地解决这两个部分,使得算法的时间复杂度优于 O(n log n)。
  2. 代码实现的细节

    • 在实现过程中,需要注意边界条件的处理,如堆的大小超过 k 时的弹出操作。
    • 通过逐步调试和测试,可以确保代码的正确性和鲁棒性。
    • 使用 Counterheapq 模块可以简化代码的实现,提高代码的可读性和可维护性。
  3. 实际应用的拓展

    • 本题的解法不仅适用于统计频率最高的 k 个元素,还可以应用于其他需要高效统计和排序的场景,如数据分析、推荐系统等。
    • 掌握哈希表和堆的基本操作和应用,对于解决类似问题具有重要的意义。
  4. 编程技巧

    • 使用 Counterheapq 模块可以简化代码的实现,提高代码的可读性和可维护性。
    • 在处理字符串时,使用 mapjoin 方法可以方便地将列表转换为字符串,元素之间用逗号分隔。
    • 通过合理的模块化设计,可以提高代码的复用性和扩展性。

通过这次学习,我不仅加深了对哈希表和堆的理解,还掌握了如何将这些数据结构应用于实际问题中。这种从理论到实践的学习过程,使我更加自信地面对各种编程挑战。在未来的学习和工作中,我将继续探索更多有趣的问题,不断提升自己的编程能力和解决问题的能力。