Sliding Window Maximum
A hard problem from Sliding Window. Try it first, open a hint when you are stuck, and read the solution when you are done.
- Hard
- Sliding Window
The problem
Given a list of integers nums and a number k, picture a window that shows k neighboring numbers. It starts at the left end of the list and moves one position to the right at a time until it reaches the right end. Return a list with the largest number in the window at each position, from left to right. Aim for a method that takes O(n) time.
What to write
function solve(nums: number[], k: number): number[]Example 1
Input nums = [1,3,-1,-3,5,3,6,7] k = 3 Output [3,3,5,5,6,7]
The six windows are [1, 3, -1], [3, -1, -3], [-1, -3, 5], [-3, 5, 3], [5, 3, 6] and [3, 6, 7]. Their largest numbers are 3, 3, 5, 5, 6 and 7.
Example 2
Input nums = [1] k = 1 Output [1]
The list holds one number and the window shows one number, so there is a single window. Its largest number is 1.
Constraints
1–10,000 integers; 1 ≤ k ≤ nums.length.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Use the constraints
There can be 10,000 numbers, and
kcan be anything up to that. Finding each window's largest number from scratch costs about 25 million comparisons whenkis 5,000. Two neighboring windows share all but one of their numbers, so most of that work is repeated.Hint 2: Ask a smaller question
A window holds 3 and then -1. While that 3 is inside the window, can the -1 be the largest? And after the 3 has left? Now a 5 arrives. Can the 3 or the -1 ever be the largest again?
Hint 3: Name the pattern
A number can be forgotten once a number at least as large arrives after it, because the newer one stays in the window longer. The candidates that remain get smaller from oldest to newest; new ones join at the back, old ones leave at the front. A queue whose values only go one way is a monotonic queue. Read the lesson on monotonic queue.
Hint 4: State the invariant
The queue holds positions that are inside the current window, oldest first, and their values get strictly smaller from front to back. So the value at the front is the largest in the window.
Hint 5: Sketch the pseudocode
Keep a queue of positions. For each position i: remove the front if it is i -
kor less. Remove from the back while the value there is not larger than the value at i. Add i at the back. If i ≥k- 1, append the front's value to the answer. Return the answer.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Approach
Keep a deque of indices with decreasing values. Remove expired indices from the front and dominated values from the back; the front is the window maximum.
The slow way
For each window, scan its k numbers and take the largest. It is correct, but neighboring windows share all but one number and the scan starts again every time. With 10,000 numbers and k = 5,000 that is about 25 million comparisons.
The insight
When a new number arrives, every earlier number that is not larger can be thrown away. The new one is at least as big and stays in the window longer. The numbers that survive are in falling order, so the largest is always the oldest survivor.
Step by step
- Keep a queue of positions whose values fall from front to back. A queue is a line: new positions join at the back and old ones leave at the front. This one also drops positions from the back, which makes it a double-ended queue, a deque for short.
- Drop the front position once it has slid out of the window. The window that ends at position i starts at i -
k+ 1, so position i -kis outside it. Only the front can be that old, because the queue is in order of arrival. - Drop from the back every value that is not larger than the new one. Such a number arrived earlier and is no bigger, so it leaves the window first and is never needed as the largest while the new one is there.
- Add the new position at the back. The position goes in, not the value, because a position also tells you when the number has left the window. Everything still in the queue is larger than the new number, so the falling order holds.
- Once the first window is full, report the value at the front. Values fall from front to back, so the front is the largest in the window. The first
k- 1 positions do not complete a window, so nothing is reported for them. - Return the maximum collected for every window. One value was reported for each position from
k- 1 to the end. That is one per window, in order from left to right.
The same numbered steps are the comments in each solution.
JavaScript
function solve(nums, k) {
// 1. Keep a queue of positions whose values fall from front to back
const q = [],
out = [];
// Removing an array's first entry moves all the others,
// so h marks the front of the queue instead.
let h = 0;
for (let i = 0; i < nums.length; i++) {
// 2. Drop the front position once it has slid out of the window
while (h < q.length && q[h] <= i - k) h++;
// 3. Drop from the back every value that is not larger than the new one
while (q.length > h && nums[q[q.length - 1]] <= nums[i]) q.pop();
// 4. Add the new position at the back
q.push(i);
// 5. Once the first window is full, report the value at the front
if (i >= k - 1) out.push(nums[q[h]]);
}
// 6. Return the maximum collected for every window
return out;
}TypeScript
function solve(nums: number[], k: number): number[] {
// 1. Keep a queue of positions whose values fall from front to back
const q = [],
out = [];
// Removing an array's first entry moves all the others,
// so h marks the front of the queue instead.
let h = 0;
for (let i = 0; i < nums.length; i++) {
// 2. Drop the front position once it has slid out of the window
while (h < q.length && q[h] <= i - k) h++;
// 3. Drop from the back every value that is not larger than the new one
while (q.length > h && nums[q[q.length - 1]] <= nums[i]) q.pop()!;
// 4. Add the new position at the back
q.push(i);
// 5. Once the first window is full, report the value at the front
if (i >= k - 1) out.push(nums[q[h]]);
}
// 6. Return the maximum collected for every window
return out;
}
Python
from collections import deque
def solve(nums, k):
# 1. Keep a queue of positions whose values fall from front to back
queue = deque()
out = []
for i, num in enumerate(nums):
# 2. Drop the front position once it has slid out of the window
while queue and queue[0] <= i - k:
queue.popleft()
# 3. Drop from the back every value that is not larger than the new one
while queue and nums[queue[-1]] <= num:
queue.pop()
# 4. Add the new position at the back
queue.append(i)
# 5. Once the first window is full, report the value at the front
if i >= k - 1:
out.append(nums[queue[0]])
# 6. Return the maximum collected for every window
return out
Example 1, traced
Input nums = [1,3,-1,-3,5,3,6,7] k = 3 Output [3,3,5,5,6,7]
| Position | Value | Dropped (values) | Queue after (values) | Maximum reported |
|---|---|---|---|---|
| 0 | 1 | Nothing | 1 | Window not full yet |
| 1 | 3 | Back: 1 | 3 | Window not full yet |
| 2 | -1 | Nothing | 3, -1 | 3 |
| 3 | -3 | Nothing | 3, -1, -3 | 3 |
| 4 | 5 | Front: 3. Back: -3, -1 | 5 | 5 |
| 5 | 3 | Nothing | 5, 3 | 5 |
| 6 | 6 | Back: 3, 5 | 6 | 6 |
| 7 | 7 | Back: 6 | 7 | 7 |
| End of the list | - | - | - | Return 3, 3, 5, 5, 6, 7 |
Why it works
The queue always holds positions inside the window, in order of arrival, with values that get strictly smaller. A number is removed only when it has left the window or when a newer number at least as large has arrived. So the window's largest value is always still in the queue, and since the values fall from front to back, it is at the front.
Time and space
Time O(n), extra space O(n) in this array-backed deque. Each position is added once and removed at most once, so the two inner loops run at most n times in all: O(n). The JavaScript queue is a plain array that only marks its front, so positions dropped there stay in memory and it can reach n entries: O(n) space.
n and m are input sizes, h is a tree's height, V and E are a graph's vertices and edges. The bounds describe this reference solution.
Watch out
- Clear the back with a loop, not a single test. One arriving number can beat several waiting ones: in example 1 the 5 removes both -3 and -1. A single test gives 3, 3, -1, 5, 5, 7.
- The front leaves when its position is i -
kor less. Testing for less than i -kkeeps it one step too long: [3, -3] withk= 1 gives [3, 3], not [3, -3]. - Report nothing until
knumbers have been read. Reporting from the first position gives example 1 eight values instead of six.
Pattern
Monotonic queue. Builds on Permutation In String.