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;
}
};