查找热点数据问题详解 | 豆包MarsCode AI刷题

123 阅读7分钟

问题描述

给你一个整数数组 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]

初步思考

对于刚看到题目,最直接的想法可能是暴力求解:

  1. 统计每个元素的出现次数:遍历数组,记录每个数字出现的次数。
  2. 排序:按照出现次数对元素进行排序。
  3. 提取前 k 个元素:取出出现次数最高的前 k 个元素。

问题:这种方法需要对元素进行排序,排序的时间复杂度是 O(n log n),而题目要求时间复杂度必须优于 O(n log n)。 为了满足时间复杂度要求,我们需要寻找更高效的方法。

可行方案

方法一:哈希表 + 桶排序

核心思想

  1. 使用哈希表统计频率。
  2. 利用桶排序的思想,将元素按照频率分配到不同的“桶”中。
  3. 从高频到低频收集前 k 个元素。
  4. 对结果进行升序排序。
详细步骤:
步骤1:统计频率
  • 哈希表(字典) : 哈希表是一种数据结构,能够在常数时间内完成插入、删除和查找操作。 在 Python 中,哈希表通常通过 dictcollections.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 = []

步骤详解:

  1. 遍历频率哈希表: 对于每个元素及其频率,执行以下操作:

    ①插入堆:将元素及其频率作为元组 (frequency, element) 插入堆中。

    ②维护堆大小:如果堆的大小超过 k,则移除堆顶元素(频率最低的元素)。

  2. 堆的大小限制: 通过始终保持堆的大小不超过 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 个高频元素。由于堆中元素的顺序并不一定按升序排列,我们需要对提取的结果进行升序排序。

步骤详解:

  1. 提取堆中的元素: 将堆中的所有元素提取到一个列表中。
  2. 排序: 对提取出来的元素按照数值升序排列。
  3. 格式化输出: 根据题目要求,将结果转换为逗号分隔的字符串。
示例操作:

堆中元素:[(2, 2), (3, 1)]
  提取元素:[2, 1]
  升序排序:[1, 2]
  格式化输出:"1,2"

优点

  1. 时间效率高:
  • 频率统计:O(n)
  • 构建堆:O(n log k)
  • 提取和排序:O(k log k)
  • 总时间复杂度:O(n log k + k log k) ≈ O(n log k)
  1. 适用于大规模数据:

    当数组规模较大且 k 相对较小时,最小堆方法表现尤为出色。

  2. 空间效率合理:

    仅需额外的 O(k) 空间来维护堆,适用于需要控制空间消耗的场景。

缺点

  1. 实现复杂度较高:

    相较于桶排序,使用堆需要更深入的理解和实现技巧。

  2. 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