
在算法面试中,“滑动窗口的最大值”是高频考点之一,它不仅考察对滑动窗口思想的理解,更考验对数据结构(如双端队列)的灵活运用。本文将基于《剑指Offer》原题,从问题分析、思路推导、算法优化到多语言实现,全方位拆解这道题,帮助你不仅会“做”,更能理解“为什么这么做”。
一、问题重述与核心难点
1. 题目定义
给定一个长度为n的整数数组num和滑动窗口大小size,窗口从数组最左侧向右滑动,每次滑动1个位置,求所有滑动窗口内的最大值。
示例:
输入数组[2,3,4,2,6,2,5,1],窗口大小3,输出[4,4,6,6,6,5]。
对应的6个滑动窗口如下:
滑动窗口 | 最大值 |
|---|---|
[2,3,4],2,6,2,5,1 | 4 |
2,[3,4,2],6,2,5,1 | 4 |
2,3,[4,2,6],2,5,1 | 6 |
2,3,4,[2,6,2],5,1 | 6 |
2,3,4,2,[6,2,5],1 | 6 |
2,3,4,2,6,[2,5,1] | 5 |
2. 核心难点
若采用“暴力解法”(每次窗口滑动后遍历窗口内元素找最大值),时间复杂度为O(n*size)——当n和size较大时(如n=1e5、size=1e3),会出现超时问题。
因此,如何降低时间复杂度是本题的关键,目标是找到一种能在O(1)时间内获取窗口最大值、O(n)时间内完成所有窗口处理的方案。
二、最优思路:双端队列(Deque)的“单调递减”应用
1. 核心思想
双端队列(Deque)支持两端的插入和删除操作,我们可以利用它维护一个“单调递减序列”,队列中存储的是数组元素的下标(而非元素值),满足以下规则:
入队规则:对于当前元素
num[i],从队列尾部开始,删除所有小于num[i]的元素下标——因为这些元素在num[i]存在的窗口中,永远不可能成为最大值(num[i]更大且更靠后,滑动时会更晚离开窗口)。出队规则:若队列头部的下标超出当前窗口的左边界(即
index.front() <= i - size),说明该元素已滑出窗口,需从头部删除。最大值获取:每个窗口的最大值对应队列头部的元素值(
num[index.front()])——因为队列是单调递减的,头部始终是当前窗口的最大元素下标。
2. 分步推导(结合示例)
以数组[2,3,4,2,6,2,5,1]、窗口大小3为例,逐步拆解双端队列的操作过程:
步骤 | 当前元素 | 当前窗口范围 | 队列内下标(对应值) | 操作说明 | 窗口是否完整 | 最大值 |
|---|---|---|---|---|---|---|
1 | 2(i=0) | [0,0] | [0(2)] | 队列空,直接入队 | 否 | - |
2 | 3(i=1) | [0,1] | [1(3)] | 3>2,删除尾部的0,入队1 | 否 | - |
3 | 4(i=2) | [0,2] | [2(4)] | 4>3,删除尾部的1,入队2 | 是 | 4(num[2]) |
4 | 2(i=3) | [1,3] | [2(4),3(2)] | 2<4,直接入队3 | 是 | 4(num[2]) |
5 | 6(i=4) | [2,4] | [4(6)] | 6>2、6>4,删除3、2,入队4 | 是 | 6(num[4]) |
6 | 2(i=5) | [3,5] | [4(6),5(2)] | 2<6,直接入队5 | 是 | 6(num[4]) |
7 | 5(i=6) | [4,6] | [4(6),6(5)] | 5>2,删除5,入队6 | 是 | 6(num[4]) |
8 | 1(i=7) | [5,7] | [6(5),7(1)] | 4已滑出窗口(4 <=7-3=4?是,删除4);1<5,入队7 | 是 | 5(num[6]) |
最终收集的最大值为[4,4,6,6,6,5],与预期结果一致。
3. 时间复杂度分析
每个元素最多入队1次、出队1次,队列操作的总时间为
O(n);遍历数组的时间为
O(n);整体时间复杂度为
O(n),空间复杂度为O(size)(队列最多存储size个元素)。
三、多语言实现(附详细注释)
1. C++ 实现(基于STL deque)
#include <vector>
#include <deque>
#include <iostream>
using namespace std;
class Solution {
public:
vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
vector<int> maxInWindows; // 存储所有窗口的最大值
deque<int> indexQueue; // 双端队列,存储数组元素的下标(维护单调递减)
// 边界条件:数组长度需>=窗口大小,且窗口大小>=1
if (num.empty() || size < 1 || num.size() < size) {
return maxInWindows;
}
// 第一步:初始化第一个窗口(前size个元素)
for (unsigned int i = 0; i < size; ++i) {
// 从队尾删除所有小于当前元素的下标(保证队列单调递减)
while (!indexQueue.empty() && num[i] >= num[indexQueue.back()]) {
indexQueue.pop_back();
}
// 当前元素下标入队
indexQueue.push_back(i);
}
// 第二步:处理后续窗口(从第size个元素开始)
for (unsigned int i = size; i < num.size(); ++i) {
// 记录当前窗口的最大值(队列头部元素)
maxInWindows.push_back(num[indexQueue.front()]);
// 1. 维护队列单调性:删除队尾小于当前元素的下标
while (!indexQueue.empty() && num[i] >= num[indexQueue.back()]) {
indexQueue.pop_back();
}
// 2. 移除滑出窗口的元素下标(头部元素超出窗口左边界)
if (!indexQueue.empty() && indexQueue.front() <= static_cast<int>(i - size)) {
indexQueue.pop_front();
}
// 3. 当前元素下标入队
indexQueue.push_back(i);
}
// 第三步:添加最后一个窗口的最大值(循环中未处理)
maxInWindows.push_back(num[indexQueue.front()]);
return maxInWindows;
}
};
// 测试代码
int main() {
Solution sol;
vector<int> num = {2, 3, 4, 2, 6, 2, 5, 1};
unsigned int size = 3;
vector<int> result = sol.maxInWindows(num, size);
// 输出结果:4 4 6 6 6 5
for (int val : result) {
cout << val << " ";
}
cout << endl;
return 0;
}2. Python 实现(基于collections.deque)
Python的collections.deque提供了双端队列的功能,操作与C++类似,代码更简洁:
from collections import deque
from typing import List
class Solution:
def maxInWindows(self, num: List[int], size: int) -> List[int]:
max_in_windows = [] # 存储结果
index_queue = deque() # 双端队列,存下标(单调递减)
# 边界条件判断
if not num or size < 1 or len(num) < size:
return max_in_windows
# 初始化第一个窗口
for i in range(size):
# 队尾删除小于当前元素的下标
while index_queue and num[i] >= num[index_queue[-1]]:
index_queue.pop()
index_queue.append(i)
# 处理后续窗口
for i in range(size, len(num)):
# 记录当前窗口最大值
max_in_windows.append(num[index_queue[0]])
# 1. 维护队列单调性
while index_queue and num[i] >= num[index_queue[-1]]:
index_queue.pop()
# 2. 移除滑出窗口的下标
if index_queue and index_queue[0] <= i - size:
index_queue.popleft()
# 3. 当前下标入队
index_queue.append(i)
# 添加最后一个窗口的最大值
max_in_windows.append(num[index_queue[0]])
return max_in_windows
# 测试
if __name__ == "__main__":
sol = Solution()
num = [2, 3, 4, 2, 6, 2, 5, 1]
size = 3
print(sol.maxInWindows(num, size)) # 输出:[4, 4, 6, 6, 6, 5]3. Java 实现(基于LinkedList模拟双端队列)
Java中无专门的Deque类,但可通过LinkedList的addLast()、removeLast()、peekFirst()等方法模拟双端队列:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class Solution {
public List<Integer> maxInWindows(int[] num, int size) {
List<Integer> maxInWindows = new ArrayList<>();
LinkedList<Integer> indexQueue = new LinkedList<>(); // 模拟双端队列
// 边界条件
if (num == null || num.length == 0 || size < 1 || num.length < size) {
return maxInWindows;
}
// 初始化第一个窗口
for (int i = 0; i < size; i++) {
// 队尾删除小于当前元素的下标
while (!indexQueue.isEmpty() && num[i] >= num[indexQueue.peekLast()]) {
indexQueue.removeLast();
}
indexQueue.addLast(i);
}
// 处理后续窗口
for (int i = size; i < num.length; i++) {
// 记录当前窗口最大值
maxInWindows.add(num[indexQueue.peekFirst()]);
// 1. 维护队列单调性
while (!indexQueue.isEmpty() && num[i] >= num[indexQueue.peekLast()]) {
indexQueue.removeLast();
}
// 2. 移除滑出窗口的下标
if (!indexQueue.isEmpty() && indexQueue.peekFirst() <= i - size) {
indexQueue.removeFirst();
}
// 3. 当前下标入队
indexQueue.addLast(i);
}
// 添加最后一个窗口的最大值
maxInWindows.add(num[indexQueue.peekFirst()]);
return maxInWindows;
}
// 测试
public static void main(String[] args) {
Solution sol = new Solution();
int[] num = {2, 3, 4, 2, 6, 2, 5, 1};
int size = 3;
System.out.println(sol.maxInWindows(num, size)); // 输出:[4, 4, 6, 6, 6, 5]
}
}四、常见问题与优化拓展
1. 边界条件处理
必须考虑以下特殊情况,否则会出现逻辑错误或空指针异常:
数组为空(
num.length == 0);窗口大小为0或1(
size < 1时返回空,size=1时直接返回原数组);数组长度小于窗口大小(
num.length < size,无有效窗口,返回空)。
2. 为什么存储“下标”而非“元素值”?
若存储元素值,无法判断该元素是否已滑出窗口(例如窗口中存在多个相同值,无法区分位置)。存储下标可通过index <= i - size直接判断是否超出窗口范围,逻辑更严谨。
3. 拓展场景:滑动窗口的最小值
若题目改为“求滑动窗口的最小值”,只需将双端队列的“单调递减”改为“单调递增”——队列头部始终是当前窗口的最小元素下标,入队时删除所有大于当前元素的下标即可。
五、总结
“滑动窗口的最大值”的核心是用双端队列维护单调序列,通过牺牲少量空间(O(size))换取时间复杂度的优化(从O(n*size)降至O(n)),是典型的“空间换时间”算法思想。
掌握该思路后,不仅能解决本题,还能应对类似的“滑动窗口极值”问题(如最小值、第k大值等),在面试中做到举一反三。建议结合上述代码手动推导一遍队列操作过程,加深对“单调队列”的理解。
1041

被折叠的 条评论
为什么被折叠?



