Topics › Stacks and Queues › Implementation Problems
Sliding Window Maximum
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an integer array nums and a window size k. A window of k consecutive elements starts at the left end and slides one position to the right at a time until it reaches the right end. Return an array with the maximum of each window, in order. There are n - k + 1 windows.
Constraints: 1 ≤ k ≤ n ≤ 10^5, -10^4 ≤ nums[i] ≤ 10^4. The Java file returns an empty array for an empty input or an invalid k.
Example 1
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
windows: [1 3 -1] [3 -1 -3] [-1 -3 5] [-3 5 3] [5 3 6] [3 6 7]
answer = [3, 3, 5, 5, 6, 7]
Example 2
nums = [9, 8, 7, 6], k = 2
answer = [9, 8, 7] // each window's maximum is its first element
Approach 1: Brute force
Intuition. Compute every window’s maximum from scratch with an inner loop. Each window shares k - 1 elements with the previous one, so most of that work is repeated, but it is the obvious correct baseline.
Algorithm.
- For every start
ifrom0ton - k: - Scan
nums[i..i+k-1]for the maximum and store it inres[i].
static int[] bruteForce(int[] nums, int k) {
int n = nums.length;
if (n == 0 || k <= 0 || k > n) return new int[0];
int[] res = new int[n - k + 1];
for (int i = 0; i + k <= n; i++) {
int mx = Integer.MIN_VALUE;
for (int j = i; j < i + k; j++) mx = Math.max(mx, nums[j]);
res[i] = mx;
}
return res;
}
Complexity. Time O(n · k): n - k + 1 windows, k elements each. In the worst case (k ≈ n/2) this is quadratic, about 2.5 × 10^9 steps for n = 10^5. Space O(1) besides the output.
Approach 2: Better, a max-heap with lazy deletion
Intuition. A max-heap gives the largest element in O(1) and accepts new elements in O(log n). The problem is removal: when the window slides, the element that leaves might be buried anywhere in the heap, and Java’s PriorityQueue.remove(Object) is linear.
The trick is to not remove it right away. Store {value, index} pairs. When we need the maximum of the window ending at i, look at the top of the heap; if its index is ≤ i - k, it has already left the window, so discard it and look again. Elements that left the window but are not on top do no harm: they can never be reported unless they reach the top, and then they are discarded.
Algorithm.
- For each
i, push{nums[i], i}. - Once the first full window exists (
i ≥ k - 1): pop while the top’s index is≤ i - k, then record the top’s value asres[i - k + 1].
static int[] better(int[] nums, int k) {
int n = nums.length;
if (n == 0 || k <= 0 || k > n) return new int[0];
int[] res = new int[n - k + 1];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(b[0], a[0])); // largest value first
for (int i = 0; i < n; i++) {
pq.offer(new int[]{nums[i], i});
if (i >= k - 1) {
while (pq.peek()[1] <= i - k) pq.poll(); // discard maxima that are no longer in the window
res[i - k + 1] = pq.peek()[0];
}
}
return res;
}
Complexity. Time O(n log n): each element is pushed once and popped at most once, O(log n) each. The heap can hold up to n entries because stale ones linger (an increasing array never pops anything), so space is O(n).
Approach 3: Optimal, monotonic deque
Intuition. Ask which elements of the current window could ever be a window maximum again, now or in the future.
Suppose j < i, both are in the window, and nums[j] ≤ nums[i]. Every future window that contains j also contains i, because i is to the right and leaves later. In all of those windows nums[i] is at least as large. So j can never again be the answer, and we can throw it away for good.
After discarding all such elements, the survivors have strictly decreasing values from oldest to newest. Store their indices in a double-ended queue:
- The front holds the oldest survivor, which is also the largest value: that is the window maximum.
- When a new element arrives, pop from the back every index whose value is
≤the new value (they are dominated), then push the new index at the back. - When the window slides, the front index may fall out of range (
≤ i - k); pop it from the front. Only one index leaves per step, and it can only be the front because the front is the oldest index in the deque.
Algorithm.
- For each
i:- If the front index is
≤ i - k, pop it from the front. - While the back value is
≤ nums[i], pop from the back. - Push
iat the back. - If
i ≥ k - 1, recordnums[front].
- If the front index is
static int[] optimal(int[] nums, int k) {
int n = nums.length;
if (n == 0 || k <= 0 || k > n) return new int[0];
int[] res = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst(); // front slid out of the window
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) dq.pollLast(); // can never be a maximum again
dq.offerLast(i);
if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
Dry run on nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 (the deque is shown as values, front first):
| i | nums[i] | expired at front | dominated at back | deque after | output |
|---|---|---|---|---|---|
| 0 | 1 | none | none | [1] | |
| 1 | 3 | none | 1 | [3] | |
| 2 | -1 | none | none | [3, -1] | 3 |
| 3 | -3 | none | none | [3, -1, -3] | 3 |
| 4 | 5 | 3 (index 1) | -3, -1 | [5] | 5 |
| 5 | 3 | none | none | [5, 3] | 5 |
| 6 | 6 | none | 3, 5 | [6] | 6 |
| 7 | 7 | none | 6 | [7] | 7 |
Complexity. Time O(n): every index enters the deque once and leaves at most once, from either end, so the inner while loop runs at most n times in total. Space O(k): the deque only holds indices from the current window.
Popping equal values (≤ rather than <) is a choice, not a requirement. Keeping equal values also works, because the front check uses indices. Popping them keeps the deque smaller.
Edge cases and pitfalls
k == 1. Every window is a single element, so the answer is the array itself.k == n. One window; the answer is the overall maximum.- Duplicates.
[4, 4, 4]withk = 2gives[4, 4]. With≤popping, the deque holds only the newest4, which is correct. - Negative numbers. Do not initialise a maximum with
0; the brute force usesInteger.MIN_VALUE, and the deque never needs a sentinel. - Store indices, not values. With values only, you cannot tell whether the front has left the window when duplicates exist.
- Output index. The window ending at
istarts ati - k + 1, which is where its answer goes.
Related problems
- Stock span problem uses the same “a newer, larger element dominates older, smaller ones” pruning on a stack.
- Next Greater Element is the stack version of the dominated-element idea.
- Maximum of Minimums for Every Window Size asks about every window size at once, using previous and next smaller elements.
- Kth largest element in a stream of running integers is a heap problem in the spirit of Approach 2.