题目解析:查找热点数据问题 | 豆包MarsCode AI刷题

49 阅读5分钟

题目解析

给定一个整数数组 nums 和一个整数 k,要求返回数组中频率前 k 高的元素。你可以假设输入数组的大小是 n,并且 k 是一个有效的整数(1 <= k <= n)。这个问题是一个典型的“频率统计”和“堆排序”结合的问题,涉及如何高效地统计元素出现的频率并选出频率最高的元素。

思路解析

1. 统计元素频率

首先,我们需要知道数组中每个元素的出现次数。这个可以通过一个哈希表(Map)来实现。哈希表可以快速地插入元素和更新计数,每个元素的出现次数存储在哈希表中,键为元素值,值为该元素的出现频率。

假设输入数组 nums[1, 1, 1, 2, 2, 3],我们可以使用哈希表统计频率。遍历数组时,对于每个元素,我们将其作为键插入哈希表,并更新对应的频率。最终得到以下统计结果:

{
  1: 3,
  2: 2,
  3: 1
}

在这个哈希表中,键是数组中的元素,值是这些元素的出现次数。

2. 使用最小堆来选出频率前 k 高的元素

一旦我们得到了每个元素的频率,接下来就需要找出频率最高的 k 个元素。为了做到这一点,可以使用 最小堆

  • 堆的特点:最小堆是一种完全二叉树结构,其中每个父节点的值都小于等于其子节点的值,因此堆顶元素是最小的。我们可以利用这一特点来始终保持堆中元素的大小为 k,并在堆中存储频率前 k 高的元素。
  • 为什么使用最小堆:由于我们需要获取频率前 k 高的元素,并且每次维护堆的大小为 k,当堆中的元素数量超过 k 时,堆顶元素(即频率最小的元素)需要被移除,确保堆中始终保存频率最高的 k 个元素。

操作流程

  1. 插入堆:遍历频率表,对于每个元素(即频率对),我们将其插入到最小堆中。
  2. 保持堆大小为 k:当堆的大小超过 k 时,移除堆顶元素(即频率最小的元素)。
  3. 返回结果:遍历堆中的所有元素,返回它们的键(即频率最高的元素)。

这样,堆最终会包含频率前 k 高的元素。

3. 整体算法的复杂度

  • 统计频率:使用哈希表统计频率的时间复杂度是 O(n),其中 n 是数组 nums 的长度。我们需要遍历整个数组,将每个元素的频率存入哈希表。
  • 堆操作:对于每个元素,我们需要将其插入到堆中,堆的大小保持为 k。每次插入操作的时间复杂度是 O(log k),因此插入 n 个元素的总时间复杂度是 O(n log k)
  • 提取结果:从堆中提取 k 个元素的时间复杂度是 O(k log k)

所以,整体的时间复杂度是 O(n log k),空间复杂度是 O(n + k),其中 n 是数组的大小,k 是要求返回的频率前 k 个元素。

代码详解

import java.util.*;

public class Main {
    public static List<Integer> solution(int[] nums, int k) {
        // Step 1: 统计每个元素的频率
        Map<Integer, Integer> frequencyMap = new HashMap<>();
        for (int num : nums) {
            frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
        }

        // Step 2: 使用一个最小堆来找频率前 k 高的元素
        // 使用 lambda 表达式来按照频率进行排序
        PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(
            (a, b) -> a.getValue() - b.getValue()  // 按频率升序排序
        );

        // Step 3: 保持堆的大小为 k
        for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
            minHeap.offer(entry);
            if (minHeap.size() > k) {
                minHeap.poll();  // 移除频率最小的元素
            }
        }

        // Step 4: 将堆中的元素添加到结果列表中
        List<Integer> result = new ArrayList<>();
        while (!minHeap.isEmpty()) {
            result.add(minHeap.poll().getKey());
        }

        return result;
    }

    public static void main(String[] args) {
        // 测试用例
        int[] nums1 = {1, 1, 1, 2, 2, 3};
        int[] nums2 = {1};

        System.out.println(solution(nums1, 2));  // 输出:[1, 2]
        System.out.println(solution(nums2, 1));  // 输出:[1]
    }
}

代码解析

  1. 统计频率

    Map<Integer, Integer> frequencyMap = new HashMap<>();
    for (int num : nums) {
        frequencyMap.put(num, frequencyMap.getOrDefault(num, 0) + 1);
    }
    

    这部分代码遍历了数组 nums,并使用哈希表 frequencyMap 存储每个数字出现的次数。getOrDefault(num, 0) 方法用来获取元素的当前频率,如果该元素还没有出现在哈希表中,则返回默认值 0,然后加 1

  2. 构造最小堆

    PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(
        (a, b) -> a.getValue() - b.getValue()  // 按频率升序排序
    );
    

    PriorityQueue 是 Java 提供的堆数据结构。这里使用了一个自定义的比较器,使得堆中的元素根据频率升序排列。堆中存储的是 Map.Entry<Integer, Integer>,即每个元素及其频率。

  3. 维护堆的大小

    for (Map.Entry<Integer, Integer> entry : frequencyMap.entrySet()) {
        minHeap.offer(entry);
        if (minHeap.size() > k) {
            minHeap.poll();  // 移除频率最小的元素
        }
    }
    

    遍历频率表,对于每个频率对,我们将其插入到堆中。如果堆的大小超过 k,就弹出堆顶元素(即频率最小的元素)。这样,堆最终将包含频率前 k 高的元素。

  4. 提取结果

    List<Integer> result = new ArrayList<>();
    while (!minHeap.isEmpty()) {
        result.add(minHeap.poll().getKey());
    }
    return result;
    

    将堆中的元素逐个取出,放入结果列表中。最终返回的列表即为频率前 k 高的元素。

示例解释

输入示例1

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

  1. 统计频率:频率表为 {1: 3, 2: 2, 3: 1}
  2. 构建堆:将元素 123 插入堆,堆中元素按频率升序排列。当堆的大小超过 2 时,移除频率最小的元素(即 3)。
  3. 结果:堆中最终包含元素 12,返回 [1, 2]

输入示例2

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

  1. 统计频率:频率表为 {1: 1}
  2. 构建堆:堆中只有一个元素 1,不需要移除任何元素。
  3. 结果:返回 [1]

总结

通过哈希表统计频率,并使用最小堆选出频率前 k 高的元素,我们能够高效地解决这个问题。整体时间复杂度为 O(n log k),非常适合大规模数据处理。