Sliding Window Maximum
Interview Prep
The problem
Given an array nums and a window size k,
return an array of length n − k + 1 where entry
i is the maximum of nums[i..i+k−1].
Why the obvious approaches fall short
A naive scan recomputes the max for every window in O(k),
giving O(n · k) total — too slow for the standard
interview constraints. A max-heap with lazy deletion gets to
O(n log n), but there's an O(n) solution
using a deque that's worth memorising because the same pattern
solves a whole family of problems.
def max_window_brute(nums: list[int], k: int) -> list[int]:
return [max(nums[i:i+k]) for i in range(len(nums) - k + 1)]
# O(n * k) — TLE for large inputs. Pattern: the monotonic deque
Keep a deque of indices, maintained so the corresponding
values are strictly decreasing from front to back.
When a new element enters: pop from the back everyone smaller than
or equal to it (they can't be the maximum of any future window that
also contains the new element). When the front of the deque falls
outside the current window, drop it. The maximum of the current
window is always at nums[dq[0]].
Every index is pushed once and popped once, so the amortized cost
per step is O(1). Total: O(n).
Optimal solution
from collections import deque
def max_window(nums: list[int], k: int) -> list[int]:
"""O(n). Monotonic deque of indices, values decreasing front-to-back."""
dq = deque() # indices, nums[dq] strictly decreasing
out = []
for i, x in enumerate(nums):
# 1) Drop indices outside the window
if dq and dq[0] <= i - k:
dq.popleft()
# 2) Maintain decreasing property: anyone smaller than x can never be the max again
while dq and nums[dq[-1]] <= x:
dq.pop()
dq.append(i)
# 3) Once the first full window is reached, the front is the answer
if i >= k - 1:
out.append(nums[dq[0]])
return out Trace
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
i=0, x=1: dq=[0] window not full
i=1, x=3: pop 0 (1<=3); dq=[1] not full
i=2, x=-1: dq=[1, 2] out=[3]
i=3, x=-3: pop none; dq=[1,2,3] out=[3, 3]
i=4, x=5: dq[0]=1 == 4-3, pop front; dq=[2,3]
pop 3 (-3<=5), pop 2 (-1<=5); dq=[4] out=[3, 3, 5]
i=5, x=3: dq=[4, 5] out=[3, 3, 5, 5]
i=6, x=6: pop 5 (3<=6), pop 4 (5<=6); dq=[6] out=[3, 3, 5, 5, 6]
i=7, x=7: pop 6 (6<=7); dq=[7] out=[3, 3, 5, 5, 6, 7]
result = [3, 3, 5, 5, 6, 7] Heap variant (also acceptable)
import heapq
def max_window_heap(nums, k):
"""O(n log n). Heap of (-value, index)."""
heap, out = [], []
for i, x in enumerate(nums):
heapq.heappush(heap, (-x, i))
# Lazy deletion of out-of-window entries from the top
while heap[0][1] <= i - k:
heapq.heappop(heap)
if i >= k - 1:
out.append(-heap[0][0])
return out Complexity
- Time:
O(n)with the monotonic deque,O(n log n)with a heap. - Space:
O(k)— the deque holds at most one window's worth of indices.
Variations worth knowing
- Sliding Window Minimum — identical algorithm, comparison flipped. Worth working through separately because the min version is the inner loop of many DP optimizations.
- Largest Rectangle in Histogram — a different monotonic-stack pattern. For each bar, find the nearest smaller bar to its left and right. Same "amortised push/pop once" analysis.
- Shortest Subarray with Sum ≥ K — monotonic deque over prefix-sum indices. The plain sliding-window fails with negative numbers; the deque is what saves it.
- Constrained subsequence sum: DP where
dp[i] = nums[i] + max(0, max(dp[i−k..i−1])). The inner max is a sliding window — use the same deque trick to keep the DPO(n).