第一次接触滑动窗口问题时,是在处理一个实时数据流中的最大值跟踪需求。当时的数据量达到每秒数万条记录,传统的暴力解法完全无法满足性能要求。经过反复尝试,最终发现单调队列与滑动窗口的结合堪称绝配,不仅将时间复杂度从O(nk)优化到O(n),还保持了极低的空间复杂度。
滑动窗口本质上是在线性数据结构(如数组、链表)上维护一个动态变化的子区间。这个子区间可以固定长度(固定窗口),也可以根据条件伸缩(可变窗口)。而单调队列则是一种特殊的双端队列,它能够在O(1)时间内获取当前窗口的极值,这得益于其精心设计的入队出队规则。
关键认知:单调队列维护的并不是所有窗口元素,而是可能成为未来窗口最值的候选元素。这种选择性保留正是其高效的核心。
以最大值为例,单调递减队列的维护遵循以下原则:
python复制def maxSlidingWindow(nums, k):
from collections import deque
q = deque()
res = []
for i, num in enumerate(nums):
while q and nums[q[-1]] <= num: # 维护单调性
q.pop()
q.append(i)
if q[0] == i - k: # 移除越界元素
q.popleft()
if i >= k - 1:
res.append(nums[q[0]])
return res
这个实现中有三个关键点需要注意:
看似嵌套循环的结构实际每个元素只会入队出队各一次。对于n个元素的数组:
因此时间复杂度严格为O(n),比暴力法的O(nk)有质的飞跃。空间复杂度方面,队列最多存储k个元素,故为O(k)。
当问题升级到二维矩阵时(如求矩阵中所有大小为k×k的子矩阵的最大值),直接套用一维解法会导致O(mnk)的时间复杂度。这时需要采用分层处理的策略:
首先对每行单独应用一维滑动窗口:
python复制def preprocess_rows(matrix, k):
row_max = []
for row in matrix:
q = deque()
row_res = []
for i, num in enumerate(row):
while q and row[q[-1]] <= num:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
row_res.append(row[q[0]])
row_max.append(row_res)
return row_max
将行处理结果视为新矩阵,再对每列应用同样的方法:
python复制def maxSlidingMatrix(matrix, k):
if not matrix or not matrix[0]:
return []
# 行处理
row_max = preprocess_rows(matrix, k)
# 列处理
m, n = len(matrix), len(matrix[0])
res = []
for j in range(n - k + 1):
q = deque()
col_res = []
for i in range(m):
num = row_max[i][j]
while q and row_max[q[-1]][j] <= num:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
col_res.append(row_max[q[0]][j])
res.append(col_res)
return list(zip(*res)) # 转置回原行列顺序
这种行列分解的方法将时间复杂度优化到O(mn),空间复杂度为O(mn),是处理二维滑动窗口的标准范式。
虽然Python的collections.deque使用方便,但在性能敏感场景可以考虑:
测试表明,在LeetCode 239题中,手动实现的队列比deque快约15%:
python复制class FixedQueue:
def __init__(self, capacity):
self.arr = [0] * capacity
self.head = self.tail = 0
def push(self, val):
self.arr[self.tail] = val
self.tail += 1
def pop_left(self):
self.head += 1
def pop_right(self):
self.tail -= 1
def first(self):
return self.arr[self.head]
def last(self):
return self.arr[self.tail-1]
python复制if k > len(matrix) or k > len(matrix[0]):
return [[]] # 或 raise ValueError("k exceeds matrix dimensions")
当处理超大矩阵时,可以逐块处理避免同时存储所有中间结果:
python复制def maxSlidingMatrix_optimized(matrix, k):
if not matrix: return []
m, n = len(matrix), len(matrix[0])
res = [[0]*(n-k+1) for _ in range(m-k+1)]
# 逐列处理
for j in range(n):
col = [matrix[i][j] for i in range(m)]
col_max = slidingWindowMax(col, k)
for i in range(len(col_max)):
res[i][j] = col_max[i]
# 逐行处理结果
final = []
for row in res:
row_max = slidingWindowMax(row, k)
final.append(row_max)
return final
在图像卷积操作中,经常需要计算每个像素邻域内的统计量。例如:
python复制def maxPooling2D(image, pool_size):
return maxSlidingMatrix(image, pool_size)
处理卫星影像时,常需要计算一定范围内的地表特征极值:
在量化交易中,滑动窗口极值可用于:
python复制def get_breakout_signals(prices, window):
highs = slidingWindowMax(prices, window)
lows = slidingWindowMin(prices, window)
signals = []
for i in range(len(prices)):
if prices[i] > highs[i]:
signals.append(1) # 上突破
elif prices[i] < lows[i]:
signals.append(-1) # 下突破
else:
signals.append(0)
return signals
有时需要同时维护最大值和最小值,可以通过组合两个单调队列实现:
python复制class MinMaxQueue:
def __init__(self):
self.min_q = deque()
self.max_q = deque()
def push(self, idx, val):
# 维护最小队列
while self.min_q and val <= self.min_q[-1][1]:
self.min_q.pop()
# 维护最大队列
while self.max_q and val >= self.max_q[-1][1]:
self.max_q.pop()
self.min_q.append((idx, val))
self.max_q.append((idx, val))
def get_range(self, left):
# 移除越界元素
while self.min_q[0][0] < left:
self.min_q.popleft()
while self.max_q[0][0] < left:
self.max_q.popleft()
return self.min_q[0][1], self.max_q[0][1]
当窗口元素具有不同权重时,需要调整队列比较逻辑:
python复制def weightedSlidingWindow(nums, weights, k):
q = deque()
res = []
for i in range(len(nums)):
# 比较时考虑权重因素
while q and nums[i] * weights[i] >= nums[q[-1]] * weights[q[-1]]:
q.pop()
q.append(i)
if q[0] == i - k:
q.popleft()
if i >= k - 1:
res.append(nums[q[0]])
return res
对于窗口大小可变的情况,可以通过二分查找确定有效范围:
python复制def dynamicWindowMax(nums, condition_func):
q = deque()
left = 0
res = []
for right in range(len(nums)):
while q and nums[right] >= nums[q[-1]]:
q.pop()
q.append(right)
# 动态调整左边界
while left <= right and not condition_func(nums[left:right+1]):
if q[0] == left:
q.popleft()
left += 1
if q and left <= right:
res.append(nums[q[0]])
return res
在实际工程实践中,我发现这类算法最易出错的地方在于边界条件的处理。特别是在二维情况下,行列索引的转换很容易出现off-by-one错误。建议在实现时先在纸上画出小规模矩阵的处理过程,逐步验证每个中间步骤的正确性。