上周有个学弟把一道题目的截图发给我,标题是“剑指offer-64、滑动窗口的最大值”。他说自己用双层循环写完了,样例也过了,但提交到力扣却提示超时。我看了一眼代码,问题非常典型——在没有题目数据范围的情况下,很容易写出能跑但不合格的解法。滑动窗口的最大值这道题,几乎可以称为面试中“数组 + 双端队列”的组合经典,它的本质不是让你用 max() 函数扫窗口,而是要设计一种能随窗口移动而自然更新的数据结构。
简单描述一下题目:给定一个数组 nums 和滑动窗口大小 k,窗口每次从数组最左端移动到最右端,每次移动一位,要求返回一个数组,内容是从左到右每个窗口中的最大值。如果你正在准备算法面试、春招秋招,或者只是想弄懂单调队列,这篇内容会从头帮你说清楚:暴力为什么慢、单调队列为什么快、代码怎么写才不翻车、面试官可能追问什么。
我见过不少候选人,这道题能给出单调队列解法的,基本都能把栈、队列、双指针这一串知识串起来。反之,如果只会背模板,一旦面试官把 k 改成 1、把数组拉长、或者问“队列里为什么存下标”,能对答如流的人少之又少。这篇文章就是按我自己的理解,把这个题拆成一层层讲明白。标题里的剑指offer-64,我更愿意把它看成一道面试真题,而不是一道需要背答案的编号题。
1. 题目重述:返回每个滑动窗口内的最大值
1.1 题目描述与一个具体例子
不要嫌我啰嗦,先把题面说清楚。题目会给你一个整数数组,比如 [1, 3, -1, -3, 5, 3, 6, 7],再给你一个窗口大小 k = 3。窗口从左往右滑,每次移动一格,停在每个位置时,窗口里都有连续的 k 个数,你的任务是把这个窗口里的最大值依次记录下来。
以下面这个例子来看:
| 窗口位置 | 窗口内元素 | 窗口最大值 |
|---|---|---|
| 第一个窗口 | [1, 3, -1] | 3 |
| 第二个窗口 | [3, -1, -3] | 3 |
| 第三个窗口 | [-1, -3, 5] | 5 |
| 第四个窗口 | [-3, 5, 3] | 5 |
| 第五个窗口 | [5, 3, 6] | 6 |
| 第六个窗口 | [3, 6, 7] | 7 |
最终返回的数组就是 [3, 3, 5, 5, 6, 7]。
有些题目变体还会说“如果数组为空返回空结果”,这些边界后面专门单开一节讲。你只需要先抓住核心:数组长度是 n,返回数组长度一定是 n - k + 1,因为窗口总数就是这么多。
1.2 这题真正想考察什么
如果只是要一个结果,很多编程语言自带 max 函数,最笨的办法就是每个窗口都调一次 max,代码写出来非常短。但面试官不会因为你写出这种解法就放过你。
这道题被《剑指Offer》收录,放在“栈与队列”专题附近,目的是考察你是否理解队列这种先进先出的结构和“滑动窗口”的匹配关系。窗口每次向右移动,左侧出去一个元素,右侧进来一个元素,这和队列的出队、入队操作一模一样。难点在于:队列不只要管元素进出的顺序,还要能快速回答“当前队列里的最大值是谁”。
如果只维护一个变量记录当前最大值,窗口右移时一旦这个最大值被移出窗口,你无法立刻知道下一个最大值是谁。所以需要队列里存的不是一个元素,而是一组候选元素,并且这组候选元素要按“对答案的贡献程度”排序。能担起这个任务的数据结构,就是单调队列。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 暴力解法能过样例,但天花板太低
2.1 直接扫描每个窗口的写法
先别想着一步到位写奇技淫巧。任何一个面试者在看到题目后,至少应该能先说出最简单的解法:枚举每个窗口起点,然后在这个窗口范围内线性扫描找最大值。
用 C++ 写大概是这样:
cpp复制class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
int n = nums.size();
for (int i = 0; i + k <= n; i++) {
int curMax = nums[i];
for (int j = i; j < i + k; j++) {
curMax = max(curMax, nums[j]);
}
ans.push_back(curMax);
}
return ans;
}
};
内层循环每次从窗口起点扫到终点,找出最大值。这个代码在 k 很小的时候跑起来没问题,例如 n=10000、k=10 时,只需要大约 10 万次比较,毫秒级就能结束。
