Sliding Window Maximum
If you can derive this solution from first principles, you’ve learned the core monotonic queue pattern.
Pattern Summary
This problem uses the [Monotonic queue] pattern.
- Recognition: Forward-sliding window of fixed width $k$, rolling maximum query, older values become permanently irrelevant once beaten by newer values.
- Core Invariant: The deque stores only undominated candidates, strictly decreasing by value and increasing by index.
- Transformation: Replace $O(k)$ brute-force rescanning or $O(\log k)$ lazy heap purges with $O(1)$ amortized eager dominance eviction.
$\rightarrow$ Read the full theoretical foundation: [Monotonic queue]
Real-World Analogues
Infrastructure
Rolling Peak Request Latency
An Envoy or NGINX edge proxy records request latency continuously. Operations dashboards display the highest latency observed during the last 30 seconds. As each new request arrives, the oldest request expires. Recomputing the maximum for every update burns CPU because consecutive windows overlap almost completely.
Distributed Systems & Telemetry
Maximum Queue Depth
A distributed task scheduler tracks the deepest worker queue backlog over the last five minutes to detect bursty traffic without rescanning historical metrics every second.
Security & Rate Limiting
Peak Authentication Failures
An API gateway tracks the largest burst of failed login attempts across a moving 5-minute window to detect credential-stuffing attacks in real time without batch aggregation lag.
Interview Abstraction
LeetCode 239 strips away the network headers and queue metadata to ask the exact underlying mathematical question: given an array nums and a window size k, return the maximum value in every forward-sliding window of length k in $O(n)$ total time.
Why This Problem Matters
This problem isolates one foundational idea: permanent dominance.
Because there are no complex state transitions or auxiliary transformations, it is the purest environment to master the core deque mechanics before moving to DP or prefix sums.
| What This Problem Teaches | What It Does NOT Teach |
|---|---|
| ✓ Eager dominance eviction | ✗ Dynamic programming (see Jump Game VI) |
| ✓ Amortized $O(1)$ candidate lifecycle | ✗ Prefix sum transformations (see Shortest Subarray $\ge K$) |
| ✓ Strict monotonic invariant | ✗ Multi-deque tracking (see Continuous Subarrays) |
| ✓ FIFO positional expiry | ✗ Non-linear state graphs |
Recognition
Reach for this pattern when:
- ✓ The window slides strictly forward (left-to-right, never rewinds).
- ✓ You must report the maximum at every step.
- ✓ Rescanning the window feels wasteful ($O(nk)$ bottleneck).
- ✓ A max-heap feels close but incurs unnecessary $O(\log k)$ overhead.
The Mental Shift
Naive Thought → "Find the maximum element inside each active window."
Correct Thought → "Delete older elements the moment a larger or equal newcomer arrives."
Once element $B$ arrives behind element $A$ with $B \ge A$, element $A$ is permanently dead. Every future window covering $A$ will also cover $B$.
Solution Evolution
| Approach | Time | Space | Core Idea | Why It Fails / Bottleneck |
|---|---|---|---|---|
| 1. Brute Force | $O(nk)$ | $O(1)$ | Rescan window with max() | Scans $k-1$ duplicate elements per slide; TLE at $n, k \approx 10^5$. |
| 2. Max-Heap | $O(n \log k)$ | $O(k)$ | Track (-val, idx) with lazy eviction | Expired elements linger inside until they surface; $O(\log k)$ per update. |
| 3. Monotonic Deque | $O(n)$ | $O(k)$ | Eagerly prune dominated tails | Optimal; candidate lifecycle is strictly amortized $O(1)$. |
Why This Beats a Heap
The heap answers: “What is the largest value I have seen?”
The deque answers: “What is the largest value that is still relevant?”That single distinction eliminates the need for lazy deletion and drops every window update from $O(\log k)$ to amortized $O(1)$.
From Pattern → Code
Every monotonic queue problem maps to three explicit policy decisions:
# 1. EVICT: Prune tail candidates that are dominated by incoming value x
while dq and nums[dq[-1]] <= x:
dq.pop()
# 2. STORE: Store index so we can verify positional window expiry
dq.append(i)
# 3. EXPIRE: Drop front if its position slid out of the active window
if dq[0] <= i - k:
dq.popleft()
Optimal Solution
from collections import deque
from typing import List
def max_sliding_window(nums: List[int], k: int) -> List[int]:
if not nums or k <= 0 or k > len(nums):
return []
if k == 1:
return nums[:]
dq = deque() # stores indices; values nums[dq] strictly decreasing
out = []
for i, x in enumerate(nums):
# 1. Evict dominated candidates from tail
while dq and nums[dq[-1]] <= x:
dq.pop()
# 2. Append current candidate index
dq.append(i)
# 3. Expire head if out of window
if dq[0] <= i - k:
dq.popleft()
# 4. Record answer once first full window is reached
if i >= k - 1:
out.append(nums[dq[0]])
return out
Visual Walkthrough
Tracing nums = [1, 3, -1, -3, 5, 3, 6, 7] with k = 3.
Band W is the active window; caret M is the front of the deque (the window’s max).
Complexity Analysis
| Metric | Complexity | Rationale |
|---|---|---|
| Time | $O(n)$ | Every index enters the deque exactly once and is evicted at most once. Total operations $\le 2n$. |
| Space | $O(k)$ | The deque stores at most $k$ candidate indices during any single window step. Output array is $O(n - k + 1)$. |
Edge Cases
| Dimension | Test Case | Expected Behavior |
|---|---|---|
| Size Extremes | nums = [1], k = 1 | Immediate single-element return [1]. |
| Window Boundary | nums = [2, 1, 5, 3], k = 4 | Window spans entire array; returns single global max [5]. |
| Monotonic Input | nums = [7, 6, 5, 4, 3], k = 2 | Strictly decreasing: zero tail evictions; deque holds $k$ elements; returns [7, 6, 5, 4]. |
| Duplicate Values | nums = [4, 4, 4, 4], k = 2 | <= eviction refreshes candidate indices; returns [4, 4, 4]. |
| Negative Values | nums = [-7, -8, -2, -5], k = 2 | Correctly tracks least-negative peak; returns [-7, -2, -2]. |
| Defensive Inputs | k <= 0 or k > len(nums) | Gracefully returns empty array []. |
Common Mistakes
- ❌ Storing values instead of indices: Storing raw values makes positional expiry (
dq[0] <= i - k) impossible. - ❌ Emitting before the window is full: Emitting before index
k - 1leaks invalid partial-window extrema. - ❌ Expiring after recording: Recording the answer before checking
dq[0] <= i - kreturns stale, expired elements. - ❌ Using
<instead of<=: Allows duplicate equal values to accumulate, bloating deque memory without refreshing the expiration position. - ❌ Confusing the deque with the window: Expecting
len(dq) == k. The deque only contains the undominated frontier, not the entire window.
Problem Progression
Mastering this problem enables you to solve a progression of harder variations:
- Sliding Window Minimum (Medium): Invert eviction rule to
>= x. - Continuous Subarrays (Medium): Maintain two concurrent monotonic deques (min and max) over a variable-length sliding window.
- Jump Game VI (Medium): Apply monotonic deque over dynamic programming transitions ($O(n)$ DP optimization).
- Constrained Subsequence Sum (Hard): DP transition optimization with non-negative lower bounds.
- Shortest Subarray with Sum at Least K (Hard): Monotonic deque over prefix sums with greedy left-pointer pops.
Principal Lens
If you remember one thing: The deque is not the window. It is the frontier of undominated candidates. Once an arrival is larger than an older neighbor, the older neighbor is permanently dead.
Appendix: Design Evolution & Alternative Implementations
Why include alternative solutions?
The goal isn’t to memorize multiple implementations. It is to understand the sequence of design decisions that leads from a straightforward baseline to one that exploits stronger invariants.
Production / Practical Context
| Approach | Context |
|---|---|
| Brute Force | Tiny inputs ($k \le 10$), rapid prototyping, correctness oracle for fuzz-testing. |
| Heap (Lazy Deletion) | Arbitrary priority updates, out-of-order streams, when dominance does not hold. |
| Monotonic Deque | Fixed or variable forward-sliding windows requiring continuous rolling extrema. |
A.1 · Brute Force Rescan ($O(nk)$ Time · $O(1)$ Space)
This implementation serves as the correctness baseline. It computes each window independently, rescanning $k - 1$ unchanged elements on every slide.
def max_sliding_window_brute(nums: list[int], k: int) -> list[int]:
if not nums or k <= 0 or k > len(nums):
return []
return [max(nums[i : i + k]) for i in range(len(nums) - k + 1)]
A.2 · Heap with Lazy Deletion ($O(n \log k)$ Time · $O(k)$ Space)
Because a binary heap only supports $O(1)$ inspection and $O(\log n)$ deletion of its root, elements that slide out of the window cannot be removed immediately from the heap’s interior. Instead, we store (-val, idx) pairs and lazily purge stale entries only when they surface at the top.
import heapq
def max_sliding_window_heap(nums: list[int], k: int) -> list[int]:
if not nums or k <= 0 or k > len(nums):
return []
heap = [] # (-value, index) for max-heap
out = []
for i, x in enumerate(nums):
heapq.heappush(heap, (-x, i))
if i >= k - 1:
# Remove expired maxima until the root belongs to the current window
while heap[0][1] <= i - k:
heapq.heappop(heap)
out.append(-heap[0][0])
return out
A.3 · Comparative Evolution Matrix
| Approach | Time | Space | Design Leap | Why Replace It? |
|---|---|---|---|---|
| Brute Force | $O(nk)$ | $O(1)$ | Recompute everything from scratch | Repeats almost identical work across overlapping windows |
| Max-Heap | $O(n \log k)$ | $O(k)$ | Preserve candidates with a generic priority queue | Retains obsolete history; requires $O(\log k)$ lazy purges |
| Monotonic Deque | $O(n)$ | $O(k)$ | Exploit dominance to delete obsolete state eagerly | Optimal; candidate lifecycle is strictly amortized $O(1)$ |