Sliding Window Maximum (LC 239)
On this page
- Problem Statement
- 1. Algorithm & Pseudocode
- 2. Step-by-Step Analysis (Beginner-Friendly)
- 3. The Dry Run
- Visual — the deque as a “descending staircase”
- 4. Java Solution
- Brute Force
- Better: Max-Heap with lazy deletion
- Optimal: Monotonic Deque
- 5. The “Java vs. Others” Edge
- 6. Complexity Summary
- 7. Edge Cases & Follow-Ups
- Related Problems
Pattern: Monotonic Deque
Difficulty: Hard
Key Concept: An element that is smaller than a later element can never be the window max again — evict it immediately. What survives is a decreasing deque of indices whose front is always the answer.
Problem Statement
Given nums[] and a window size k, the window slides one position at a time from left to
right. Return an array of the maximum in each window position.
Input: int[] nums, int k, 1 <= k <= nums.length <= 10^5
Output: int[] of length n - k + 1
Example
nums = [1,3,-1,-3,5,3,6,7], k = 3 → [3,3,5,5,6,7]
[1 3 -1] -3 5 3 6 7 → 3
1 [3 -1 -3] 5 3 6 7 → 3
1 3 [-1 -3 5] 3 6 7 → 5
1 3 -1 [-3 5 3] 6 7 → 5
1 3 -1 -3 [5 3 6] 7 → 6
1 3 -1 -3 5 [3 6 7] → 7
1. Algorithm & Pseudocode
Brute force
for i from 0 to n-k:
max = -infinity
for j from i to i+k-1:
max = max(max, nums[j])
res[i] = max
Better (max-heap with lazy deletion)
heap of {value, index}, ordered by value descending
for i from 0 to n-1:
heap.push({nums[i], i})
while heap.top.index <= i - k: heap.pop() // discard stale entries
if i >= k-1: res[i-k+1] = heap.top.value
Optimal (monotonic deque)
dq = empty deque of INDICES, values decreasing front→back
for i from 0 to n-1:
1. EVICT FRONT : if dq.front <= i - k, pollFirst // slid out of window
2. EVICT BACK : while nums[dq.back] <= nums[i], pollLast // dominated forever
3. PUSH : offerLast(i)
4. RECORD : if i >= k-1, res[i-k+1] = nums[dq.front]
2. Step-by-Step Analysis (Beginner-Friendly)
-
Why the brute force fails Every window re-reads all
kelements and throws the work away. Withn = 10^5andk = 10^4that’s ~10^9 comparisons. TLE. -
First improvement: a heap Keep a max-heap of
{value, index}. The top might be stale (already slid out), so pop while the top’s index is out of range. This is lazy deletion — you never search the heap for an element to remove, you just ignore it until it surfaces. O(n log n), and a perfectly respectable answer to give first. -
The key observation that beats the heap Suppose
j < iandnums[j] <= nums[i]. Any window containingjand still valid must also containi(sinceiis later and the window slides right). Sonums[j]can never be the max again —nums[i]dominates it forever. Deletejon the spot. No heap needed, no comparisons deferred. -
What that leaves you with After applying that rule, the surviving indices always have strictly decreasing values from front to back. Therefore the front is the maximum of everything currently in play. Reading the answer is O(1).
-
Two different evictions, don’t confuse them
- Front eviction is about time: index has fallen out of the window (
dq.peekFirst() <= i - k). - Back eviction is about value: dominated by the incoming element. They are independent and both are necessary.
- Front eviction is about time: index has fallen out of the window (
-
Store indices, not values You need the index to decide whether the front has slid out of the window. Values alone can’t tell you that.
-
Why O(n) Each index is
offerLast-ed exactly once and removed at most once (from either end). Total deque operations ≤ 2n.
3. The Dry Run
nums = [1,3,-1,-3,5,3,6,7], k = 3
| i | nums[i] | Front evict | Back evict | Deque (indices) | Window max |
|---|---|---|---|---|---|
| 0 | 1 | — | — | [0] |
(not full) |
| 1 | 3 | — | pop 0 (1 <= 3) |
[1] |
(not full) |
| 2 | -1 | — | — (3 > -1) |
[1,2] |
nums[1] = 3 |
| 3 | -3 | — | — (-1 > -3) |
[1,2,3] |
nums[1] = 3 |
| 4 | 5 | pop front 1 (1 <= 4-3=1) |
pop 3, pop 2 | [4] |
nums[4] = 5 |
| 5 | 3 | — | — (5 > 3) |
[4,5] |
nums[4] = 5 |
| 6 | 6 | — | pop 5, pop 4 | [6] |
nums[6] = 6 |
| 7 | 7 | — | pop 6 | [7] |
nums[7] = 7 |
Result: [3, 3, 5, 5, 6, 7] ✓
Visual — the deque as a “descending staircase”
after i=3: front back
[ idx1 ][ idx2 ][ idx3 ]
val 3 val -1 val -3 ← strictly decreasing, front = current max
i=4 arrives with value 5:
• idx1 has slid out of the window → pollFirst
• 5 dominates -1 and -3 → pollLast, pollLast
[ idx4 ]
val 5 ← the staircase collapsed to one step
4. Java Solution
Brute Force
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] res = new int[n - k + 1];
for (int i = 0; i + k <= n; i++) {
int max = Integer.MIN_VALUE;
for (int j = i; j < i + k; j++) max = Math.max(max, nums[j]);
res[i] = max;
}
return res;
}
}
Time O(n·k) · Space O(1) extra
Better: Max-Heap with lazy deletion
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] res = new int[n - k + 1];
// {value, index}; max-heap by value. Integer.compare avoids b[0]-a[0] overflow.
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(b[0], a[0]));
for (int i = 0; i < n; i++) {
pq.offer(new int[]{nums[i], i});
while (pq.peek()[1] <= i - k) pq.poll(); // drop stale tops
if (i >= k - 1) res[i - k + 1] = pq.peek()[0];
}
return res;
}
}
Time O(n log n) · Space O(n)
Optimal: Monotonic Deque
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] res = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // INDICES; nums values decreasing front→back
for (int i = 0; i < n; i++) {
// 1. front has slid out of the window
if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();
// 2. nums[i] dominates everything smaller at the back — they can never win again
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) dq.pollLast();
// 3. this index is a live candidate
dq.offerLast(i);
// 4. once the first full window exists, the front IS the max
if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
}
Time O(n) · Space O(k) — the deque never holds more than k indices
Sliding-window minimum is the identical code with
<=flipped to>=in step 2.
5. The “Java vs. Others” Edge
ArrayDequeis the right class — O(1) at both ends, array-backed (cache-friendly), and unsynchronized.LinkedListalso implementsDequebut allocates a node per element and chases pointers; measurably slower on a 10^5 hot loop.peekFirst/pollFirst/peekLast/pollLastreturnnullon empty;getFirst/removeFirstthrow. Use the null-returning family plus anisEmpty()guard — but the guard must come first in the&&, because unboxing anullIntegerthrows NPE.- Comparator overflow:
(a, b) -> b[0] - a[0]silently breaks when values straddleInteger.MAX_VALUE/MIN_VALUE. AlwaysInteger.compare(b[0], a[0]). Interviewers plant this. PriorityQueueis not FIFO and its iterator is not in heap order — onlypoll()gives ordering. Don’t iterate a heap expecting sorted output.- Boxing:
Deque<Integer>boxes indices above 127. Anint[] dq = new int[n]withhead/tailcursors is the allocation-free version — worth naming if asked to optimise.
6. Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute force | O(n·k) | O(1) | Rescans every window; TLE |
| Max-heap + lazy deletion | O(n log n) | O(n) | Heap can hold stale entries |
| Monotonic deque | O(n) | O(k) | Each index enters/leaves once |
7. Edge Cases & Follow-Ups
k == 1→ the answer isnumsitself; the deque holds one index at a time.k == n→ a single window; deque work reduces to finding the global max.- All equal values (
[5,5,5,5]) — with<=in the back-eviction, equal values are also evicted. Still correct, because the newer index survives longer and has the same value. - Duplicates and
<vs<=— using<keeps duplicates in the deque. Both give the right max;<=keeps the deque smaller. Know that you chose deliberately. - Follow-up: sliding window median → LC 480, needs two heaps (or a
TreeMap), not a deque — a deque can only maintain an extreme, not an order statistic.
Related Problems
| # | Problem | Difficulty | Connection |
|---|---|---|---|
| LC 1438 | Longest Subarray with Absolute Diff ≤ Limit | Medium | two deques: one max, one min |
| LC 862 | Shortest Subarray with Sum at Least K | Hard | monotonic deque over prefix sums |
| LC 1696 | Jump Game VI | Medium | DP + sliding-window max of the DP array |
| LC 480 | Sliding Window Median | Hard | two heaps / TreeMap, not a deque |
| LC 739 | Daily Temperatures | Medium | sibling monotonic stack pattern |