【Hot 100 刷题计划】 LeetCode 239. 滑动窗口最大值 | C++ 优先队列与单调队列双解法

LeetCode 239. 滑动窗口最大值 | C++ 优先队列与单调队列双解法题解

📌 题目描述

题目级别:困难 (Hard)

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

  • 示例 1:
    输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
    输出:[3,3,5,5,6,7]

💡 解题思路与代码实现

这道题是滑动窗口系列中的困难题,核心痛点在于:窗口每次向右滑动时,如何快速剔除离开窗口的旧元素,并获取当前窗口内的新最大值?

我们提供两种解法:一种是思维直观的"优先队列(大顶堆)法",另一种是面试官最期望看到的极致 O ( N ) O(N) O(N) 性能的"单调队列法"。

🚀 解法一:优先队列 + 延迟删除 (大顶堆)

最直观的想法是用一个大顶堆(priority_queue)来维护窗口内的最大值。但是优先队列不支持直接删除底层的非堆顶元素。

"延迟删除"技巧:

我们将数组的 (数值, 索引) 作为一个 pair 存入大顶堆。窗口滑动时,无脑将新元素压入堆中。如果此时堆顶的最大元素实际上已经"过期"(即它的索引 ≤ i − k \le i - k ≤i−k,滑出了当前窗口),我们就把这个堆顶元素弹出去,直到堆顶元素合法为止。

💻 C++ 代码实现
cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // priority_queue 默认是大顶堆,存储 pair<数值, 索引>
        priority_queue<pair<int, int>> pq;
        vector<int> res;

        // 1. 先将前 k 个元素入堆,初始化第一个窗口
        for (int i = 0; i < k; i++) {
            pq.emplace(nums[i], i);
        }
        res.push_back(pq.top().first);

        // 2. 窗口开始向右滑动
        for (int i = k; i < nums.size(); i++) {
            // 将新进入窗口的元素加入堆中
            pq.emplace(nums[i], i);
            
            // 核心:延迟删除。如果堆顶元素的索引已经不在窗口内了,就把它弹出去
            while (pq.top().second <= i - k) {
                pq.pop();
            }
            
            // 此时的堆顶元素一定是当前窗口内的合法最大值
            res.push_back(pq.top().first);
        }

        return res;
    }
};

🏆 解法二:单调队列 (面试终极最优解)

优先队列法的时间复杂度是 O ( N log ⁡ N ) O(N \log N) O(NlogN)。为了将时间复杂度极致压缩到 O ( N ) O(N) O(N),我们需要使用一个双端队列(deque)来维护一个严格单调递减的索引队列。

核心思想(队首踢老,队尾踢小):

  • 队尾维护单调性:新元素准备入队时,如果它比队尾的元素大,那么队尾的元素就变成了"既小又老"的废物(在未来的窗口中绝不可能是最大值),直接把队尾弹出。
  • 队首清理过期元素:如果队首元素的索引已经离开了窗口范围,就将队首弹出。
  • 经过这两步过滤,队首元素永远是当前窗口内最大值的索引。
💻 进阶 C++ 代码实现
cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> res;
        deque<int> dq; // 双端队列,存储的是元素的【索引】

        for (int i = 0; i < nums.size(); i++) {
            // 1. 队首清理(踢老):如果队首的索引已经滑出了窗口,将其弹出
            if (!dq.empty() && dq.front() <= i - k) {
                dq.pop_front();
            }

            // 2. 队尾清理(踢小):保持队列的单调递减性
            // 如果新来的数比队尾的数大,说明队尾的数永远没有出头之日,直接弹出
            while (!dq.empty() && nums[dq.back()] <= nums[i]) {
                dq.pop_back();
            }

            // 3. 将当前元素的索引加入队尾
            dq.push_back(i);

            // 4. 当窗口长度达到 k 时,开始记录答案(队首永远是最大值的索引)
            if (i >= k - 1) {
                res.push_back(nums[dq.front()]);
            }
        }

        return res;
    }
};
相关推荐
迷海2 小时前
力扣原题《打家劫舍》递归版动态规划,纯手搓,已验证,未优化
c++·leetcode·动态规划
dazzle4 小时前
机器学习算法原理与实践-入门(十一):基于PyTorch的房价预测实战
pytorch·算法·机器学习
袋鼠云数栈11 小时前
集团数字化统战实战:统一数据门户与全业态监管体系构建
大数据·数据结构·人工智能·多模态
小月球~12 小时前
天梯赛 · 并查集
数据结构·算法
仍然.12 小时前
算法题目---模拟
java·javascript·算法
6Hzlia13 小时前
【Hot 100 刷题计划】 LeetCode 118. 杨辉三角 | C++ 动态规划题解
c++·leetcode·动态规划
三道渊14 小时前
C语言:文件I/O
c语言·开发语言·数据结构·c++
kali-Myon14 小时前
CTFshow-Pwn142-Off-by-One(堆块重叠)
c语言·数据结构·安全·gdb·pwn·ctf·
潇冉沐晴14 小时前
DP——背包DP
算法·背包dp