LeetCode Day13

103 阅读3分钟

239.滑动窗口最大值

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值。 进阶: 你能在线性时间复杂度内解决此题吗? 提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

思路

这道题的关键是如何在滑动窗口中快速找到最大值。为了实现这一目标,我们可以使用一个双端队列(deque)来存储数组中的索引,而不是直接存储元素值。以下是解题的核心思路:

  1. 双端队列:我们使用一个双端队列来维护当前滑动窗口中的元素索引。这个队列的特点是它的前端始终包含当前窗口的最大值的索引。
  2. 移除旧的元素:当滑动窗口向右移动时,我们需要确保队列的前端索引对应的元素仍然在当前窗口范围内。如果不在范围内,我们就从队列的前端移除该索引。
  3. 移除小于当前元素的索引:在添加一个新元素到队列之前,我们从队列的后端移除所有小于当前元素的索引。这是因为这些元素不可能是当前窗口或任何后续窗口的最大值。
  4. 添加新的元素:将当前元素的索引添加到队列的后端。
  5. 收集结果:每次当我们处理到一个元素并且它的索引大于或等于 k - 1 时(也就是说,当滑动窗口完全形成时),我们将队列的前端索引对应的元素添加到结果数组中。这是因为队列的前端始终包含当前窗口的最大值的索引。

题解

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
         std::deque<int> dq;
         std::vector<int> res;
    for (int i = 0; i < nums.size(); ++i) {
        // 移除不在滑动窗口范围内的索引
        while (!dq.empty() && dq.front() < i - k + 1) {
            dq.pop_front();
        }

        // 移除所有小于当前元素的索引
        while (!dq.empty() && nums[dq.back()] < nums[i]) {
            dq.pop_back();
        }

        // 将当前元素的索引添加到双端队列的后端
        dq.push_back(i);

        // 如果当前元素的索引大于或等于 k - 1,则将双端队列的前端索引对应的元素添加到结果数组中
        if (i >= k - 1) {
            res.push_back(nums[dq.front()]);
        }
    }

    return res;

    }
};

347.前 K 个高频元素

给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 示例 1:

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

示例 2:

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

提示:

  • 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。
  • 你的算法的时间复杂度必须优于 O(nlogn)O(n \log n) , n 是数组的大小。
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。
  • 你可以按任意顺序返回答案。

思路

1. 统计频率

首先,我们需要知道每个元素在数组中出现了多少次。为此,我们可以使用哈希表(在C++中通常使用unordered_map)。键是数组中的元素,值是该元素的出现次数。

2. 使用优先级队列

我们的目标是找到出现次数最多的k个元素。为了高效地实现这一点,我们可以使用一个小顶堆。小顶堆是一种特殊的二叉树,其中父节点的值小于或等于其子节点的值。由于C++的priority_queue默认是大顶堆,我们需要提供一个自定义的比较函数来创建小顶堆。

为什么使用小顶堆呢?因为我们可以持续地将元素和它们的频率放入堆中,如果堆的大小超过了k,我们就可以弹出堆顶的元素(这是频率最低的元素)。这样,我们可以确保堆中始终保持频率最高的k个元素。

3. 提取结果

当我们处理完所有的元素后,堆中的元素就是出现次数最多的k个元素。我们可以从堆中弹出这些元素并将它们放入一个列表中。

这种方法的主要优点是,当我们处理每个元素时,它的时间复杂度是 (O(\log k))(因为插入和删除堆中的元素的时间复杂度是 (O(\log k)))。因此,总的时间复杂度是 (O(n \log k)),其中n是数组的长度。这比简单地对所有元素进行排序要快得多,特别是当k远小于n时。

题解

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
    // 步骤1: 使用哈希表统计每个数字的频率
    std::unordered_map<int, int> freq_map;
    for (int num : nums) {
        freq_map[num]++;
    }

    // 定义优先级队列的自定义比较函数。此函数确保队列为小顶堆。
    auto cmp = [](const auto& a, const auto& b) {
        return a.second > b.second;
    };
    // 使用自定义比较函数初始化优先级队列
    std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, decltype(cmp)> pq(cmp);

    // 步骤2: 使用频率哈希表填充优先级队列
    for (const auto& [num, freq] : freq_map) {
        pq.push({num, freq});
        // 如果优先级队列的大小超过k,则弹出最小的元素(即频率最低的元素)
        if (pq.size() > k) {
            pq.pop();
        }
    }

    // 步骤3: 从优先级队列中提取前k个高频元素
    std::vector<int> res;
    while (!pq.empty()) {
        res.push_back(pq.top().first);
        pq.pop();
    }

    return res;
    }
};