子串——滑动窗口最大值

我们用一个双端队列deque,只存数组下标,并且保证:
队头 = 当前窗口最大值的下标
队列里的数从大到小排列

普通队列(queue):只能队尾进,队头出
双端队列(deque = double ended queue)两头都能插,都能删

为什么滑动窗口必须用deque?
因为我们要做两件事:
删队头(过期的最大值)
删队尾(比新数小的,没用了)++

每一步做4件事:

1、删过期
队头如果不在窗口里,删掉。判断队头是否在当前的窗口内,关键是理解:当i=k-1(即窗口大小为k)时开始形成窗口,之后随着for循环中i的移动,窗口跟着滑动,而队列存的是数组的下标,如果队头的元素dq.front()<=i-k,i-k刚好是当前i所在窗口外的那个元素的下标,说明这个元素已经过期(不属于当前窗口)因此要弹出,dq.pop_front(),那么我们是怎么判断队头是否过期了呢:因为队列存的是下标,所以我们可以通过公式dq.front()<=i-k来判断,虽然队列里有删除操作小元素的操作,但是不影响我们对队头元素是否在窗口内的判断,我们关注的是队头是否在窗口内,确保在窗口内后我们才能把它作为当前窗口的最大值,第二步的弹出操作则是通过删除更小值来确保队头最大,也就是说我们通过前面两步来找到当前窗口的最大值

2、删小数
队尾比当前数小,永远不可能成为最大值,直接删掉。解释:为了保证队头就是当前窗口的最大值,如果此时队头的元素比当前遍历到的元素nums[i]小,就弹出:dq.pop_back()。这样才能保证队头最大。之后我们直接把通过队头的下标把元素存到结果数组中

3、加当前数
把当前下标放进队列尾部。dq.push_back(i)

4、取答案
窗口形成后,队头就是当前窗口最大值
另外只有窗口形成的时候我们才能把队头下标对应元素存入到结果数组,因此我们要加入条件判断,if(i>=k-1)

补充第二步必须通过while循环来实现的,原因是:因为新进来的数可能比队列里好几个数都大,因此需要把里面的元素都删掉,既有条件又有循环最好的实现方式就是用while循环。第一步每次窗口只移动一位,最多只会有一个元素过期,就是队头元素,因此第一步我们除了while也可以用if来实现

完整代码实现如下:

#include
#include
#include // 双端队列,必须加头文件
using namespace std;

vector maxSlidingWindow(vector& nums, int k) {

vector<int> res;       // 存最终答案
deque<int> dq;         // 存下标,保证队头永远是当前窗口最大值

for (int i = 0; i < nums.size(); i++) {
    // 1. 队头超出窗口范围,删掉
    while (!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. 窗口形成了,开始记录答案
    if (i >= k - 1) {
        res.push_back(nums[dq.front()]);
    }
}
return res;

}

// 测试
int main() {

vector<int> nums = {1,3,-1,-3,5,3,6,7};
int k = 3;
vector<int> ans = maxSlidingWindow(nums, k);

// 输出结果
for (int num : ans) {
    cout << num << " ";
}
// 输出:3 3 5 5 6 7
return 0;

}

补充:
你以为:只要队头是最大的,别的不管就行

但真实算法要求:
队列里必须只保留 “有可能成为未来最大值” 的数
所有中间比新数小的,都是垃圾,必须删掉,否则队列会无限膨胀。
而删这些垃圾,必须从队尾删
普通队列做不到,它们卡在中间,普通队列碰不到!
结果:
垃圾永远留在队列里,越来越多,
最后队列爆炸、时间复杂度变成 O (n²),直接超时。

posted @ 2026-03-27 21:59  AlexXuu  阅读(1)  评论(0)    收藏  举报