青训营X豆包MarsCode 技术训练营 | 查找热点数据问题

47 阅读5分钟

问题描述

给你一个整数数组 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 个高频元素的集合是唯一的

测试样例

样例1:

输入:nums = [1, 1, 1, 2, 2, 3], k = 2
输出:"1,2"
解释:元素 1 出现了 3 次,元素 2 出现了 2 次,元素 3 出现了 1 次。因此前两个高频元素是 1 和 2。

样例2:

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

为了高效地解决这个问题,我们需要获取数组中出现频率最高的 k 个元素,并且结果需要按升序排列。为了使算法的时间复杂度优于 O(n log n),我们可以使用一些具有线性时间复杂度的算法。以下是解题思路的详细分析:

解题思路

  1. 频率统计:我们需要统计每个数字在数组中出现的频率。可以使用哈希表(字典)来实现这一点,这样的复杂度是 O(n),其中 n 是数组的大小。
  2. 使用最小堆(Heap) :为了提取前 k 个高频元素,我们可以使用最小堆。最小堆的特点是堆顶元素是最小的,因此我们可以在堆中维护当前频率最高的 k 个元素。此步骤的复杂度是 O(m log k),其中 m 是不同元素的数量。
  3. 构造结果:从堆中取出 k 个元素后,将这些元素按升序排序,然后生成以逗号分隔的字符串。排序的复杂度是 O(k log k)。

结合以上步骤,我们可以得到总的时间复杂度为 O(n + m log k),通常情况下 m <= n。因此,这一复杂度优于 O(n log n)。

具体实现步骤

  1. 使用一个字典来统计每个元素的频率。
  2. 使用 Python 的 heapq 模块创建一个最小堆,存储频率和元素的元组(频率,元素)。
  3. 将频率放入堆中,仅保持前 k 个高频元素。
  4. 从堆中提取元素并排序,然后生成结果字符串。
from collections import Counter
import heapq

def solution(nums, k):
    # 统计频率
    freq_map = Counter(nums)
    
    # 使用最小堆保存频率最高的 k 个元素
    min_heap = []
    
    for num, freq in freq_map.items():
        heapq.heappush(min_heap, (freq, num))
        if len(min_heap) > k:
            heapq.heappop(min_heap)
    
    # 提取出堆中的元素并排序
    top_k = [heapq.heappop(min_heap) for _ in range(len(min_heap))]
    top_k.sort(key=lambda x: x[1])  # 按元素值升序排序
    
    # 生成结果字符串
    result = ",".join(str(num) for freq, num in top_k)
    return result

if __name__ == "__main__":
    # 测试用例
    print(solution([1, 1, 1, 2, 2, 3], 2))  # 输出 "1,2"
    print(solution([1], 1))                  # 输出 "1"

队列算法是计算机科学中常用的数据结构和算法,主要用于管理按顺序处理的数据。队列遵循先进先出(FIFO)的原则,通常用于任务调度、数据缓冲等诸多场景。以下是一些常见的队列算法和相关概念:

1. 基本队列操作

  • 入队(Enqueue):将一个元素添加到队列的尾部。
  • 出队(Dequeue):从队列的头部移除并返回一个元素。
  • 查看队头(Front/Peek):返回队列头部的元素,但不删除它。
  • 判断队列是否为空:检查队列中是否还有元素。
  • 获取队列大小:获取当前队列中元素的数量。

2. 循环队列

循环队列是对基本队列的改进,使用数组实现,并通过处理数组的循环性质来有效利用空间。可以避免因队列头部出队而造成的空闲空间。

3. 双端队列(Deque)

双端队列允许在队列两端进行插入和删除操作。支持:

  • 从前面或后面入队(Enqueue Front / Enqueue Rear)
  • 从前面或后面出队(Dequeue Front / Dequeue Rear)

4. 优先队列

优先队列根据优先级管理元素,而不是仅仅依赖于它们的入队顺序。常用的数据结构有:

  • (Heap):通常用最小堆或最大堆实现优先队列,以支持高效的插入和删除最大(或最小)元素操作。

5. 比较队列和栈

尽管队列和栈都是线性数据结构,但它们的操作方式不同:

  • 队列:遵循先进先出(FIFO)的原则,适合任务调度等场景。
  • :遵循后进先出(LIFO)的原则,适合需要逆序处理的场景,如递归调用、表达式求值等。

6. 队列应用

  • 任务调度:用于操作系统的任务管理。
  • 资源共享:打印任务、网络请求等。
  • 缓冲区:用于数据流(如视频流、数据包)中的缓冲管理。
  • 广度优先搜索(BFS):在图算法中,使用队列实现广度优先遍历。

7. 队列的实现

  • 数组:使用固定大小的数组实现队列,但需要考虑扩容的问题。
  • 链表:使用链表实现,具有动态大小,适应性更强。
  • 标准库:许多编程语言提供了内置的队列数据结构,如 Python 的 collections.deque、Java 的 LinkedListPriorityQueue

8. 线程安全队列

在多线程环境中,线程安全的队列非常重要。很多语言提供了线程安全的队列实现,例如:

  • Java 的 BlockingQueue
  • Python 的 queue.Queue