Maximum of every window
Given an array nums and a window size k, return the maximum of
every contiguous window of length k, from left to right.
Examples
| Input | Output | Why |
|---|---|---|
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 | [3, 3, 5, 5, 6, 7] | Six windows; the third is [-1, -3, 5]. |
nums = [9, 8, 7, 6], k = 2 | [9, 8, 7] | A falling array: each window's maximum is its first element. |
nums = [4, 2, 12, 3], k = 1 | [4, 2, 12, 3] | Windows of one element. |
Constraints
- 1 ≤ k ≤ n ≤ 10⁵
- −10⁴ ≤ nums[i] ≤ 10⁴
Hints
Hint 1
Recomputing the maximum of each window costs O(k), so O(nk) in all. What information from the previous window could be kept?
Hint 2
Once a larger element arrives, every smaller element before it can never again be a window's maximum: it leaves the window first and is beaten for as long as it stays.
Hint 3
Keep the indices of the surviving candidates in a deque, values falling from front to back. Pop smaller values from the back before pushing; pop the front when it falls out of the window. The front is the answer.
Where people lose marks
- Storing values instead of indices. With values alone you cannot tell whether the front has left the window, and duplicates make it worse.
- Popping only strictly smaller values. That works, but keeps equal values and lets the deque grow; popping values that are smaller or equal is simpler and still correct.
- Expiring the front with
< i − kinstead of≤ i − k. The window ending atistarts ati − k + 1. - Reporting before the first window is complete: the first answer belongs to index
k − 1.
Dry run on [1,3,-1,-3,5,3,6,7],3
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 8nums10i31-12-3354356677deque (values)10maximanone yet0k3
Index 0 (value 1). The first window is not complete yet.
-
Step 2 / 8nums1031i-12-3354356677deque (values)30maximanone yet0k3
Index 1 (value 3). It beats 1, which can never be a maximum again, so they leave from the back. The first window is not complete yet.
-
Step 3 / 8nums1031-12i-3354356677deque (values)30-11maxima30k3
Index 2 (value -1). The front, 3, is the maximum of window [0, 2].
-
Step 4 / 8nums1031-12-33i54356677deque (values)30-11-32maxima3031k3
Index 3 (value -3). The front, 3, is the maximum of window [1, 3].
-
Step 5 / 8nums1031-12-3354i356677deque (values)50maxima303152k3
Index 4 (value 5). It beats -3, -1, 3, which can never be a maximum again, so they leave from the back. The front, 5, is the maximum of window [2, 4].
-
Step 6 / 8nums1031-12-335435i6677deque (values)5031maxima30315253k3
Index 5 (value 3). The front, 5, is the maximum of window [3, 5].
-
Step 7 / 8nums1031-12-33543566i77deque (values)60maxima3031525364k3
Index 6 (value 6). It beats 3, 5, which can never be a maximum again, so they leave from the back. The front, 6, is the maximum of window [4, 6].
-
Step 8 / 8nums1031-12-3354356677ideque (values)70maxima303152536475k3
Index 7 (value 7). It beats 6, which can never be a maximum again, so they leave from the back. The front, 7, is the maximum of window [5, 7]. Each index entered and left the deque at most once: O(n) in all.
How to think about it
A heap of the window's elements would give the maximum in O(log k) per step,
but the elements that leave the window are awkward to remove. A sharper observation removes the
need for a heap.
Suppose j < i and nums[j] ≤ nums[i]. Every window that contains
j and is still to come also contains i, because j leaves
first. So j can never be the maximum again, and it can be discarded the moment
i arrives. The candidates that remain therefore have falling values from oldest
to newest, and the oldest is the maximum of the window.
- Push index
iat the back, after popping every index whose value is no larger. - Expire the front if it has left the window, that is if it is at most
i − k. - Report the front's value once the first full window is in place, at
i ≥ k − 1.
Each index is pushed once and popped at most once, so the whole pass is O(n),
however the values are arranged.
This is the monotonic stack of "next warmer day" with a second exit. The stack already discards dominated elements from one end; the deque adds the other end, so that elements can also leave by age.
Complexity. Time O(n) · Space O(k)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
from collections import deque
def window_maximum(nums, k):
dq = deque() # indices; their values fall from front to back
out = []
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x: # dominated: leaves sooner and is no larger
dq.pop()
dq.append(i)
if dq[0] <= i - k: # the front has left the window
dq.popleft()
if i >= k - 1:
out.append(nums[dq[0]])
return out#include <deque>
#include <vector>
std::vector<int> solve(const std::vector<int>& nums, int k) {
std::deque<int> dq; // indices; values fall from front to back
std::vector<int> out;
for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
dq.push_back(i);
if (dq.front() <= i - k) dq.pop_front(); // the front has left the window
if (i >= k - 1) out.push_back(nums[dq.front()]);
}
return out;
}class Solution {
public int[] windowMaximum(int[] nums, int k) {
int n = nums.length;
int[] dq = new int[n]; // indices in dq[head..tail); values fall front to back
int head = 0, tail = 0;
int[] out = new int[n - k + 1];
for (int i = 0; i < n; i++) {
while (tail > head && nums[dq[tail - 1]] <= nums[i]) tail--;
dq[tail++] = i;
if (dq[head] <= i - k) head++; // the front has left the window
if (i >= k - 1) out[i - k + 1] = nums[dq[head]];
}
return out;
}
}function windowMaximum(nums, k) {
const dq = new Array(nums.length); // a deque of indices in a plain array: dq[head..tail)
let head = 0, tail = 0;
const out = [];
for (let i = 0; i < nums.length; i++) {
while (tail > head && nums[dq[tail - 1]] <= nums[i]) tail--; // dominated candidates leave
dq[tail++] = i;
if (dq[head] <= i - k) head++; // the front has left the window
if (i >= k - 1) out.push(nums[dq[head]]);
}
return out;
}use std::collections::VecDeque;
fn solve(nums: &[i64], k: i64) -> Vec<i64> {
let k = k as usize;
let mut dq: VecDeque<usize> = VecDeque::new(); // indices; values fall front to back
let mut out = Vec::new();
for i in 0..nums.len() {
while let Some(&j) = dq.back() {
if nums[j] > nums[i] {
break;
}
dq.pop_back();
}
dq.push_back(i);
if dq[0] + k <= i {
dq.pop_front(); // the front has left the window
}
if i + 1 >= k {
out.push(nums[dq[0]]);
}
}
out
}import Data.Array
import qualified Data.Sequence as Seq
import Data.Sequence (ViewL(..), ViewR(..), viewl, viewr, (|>))
solve :: [Int] -> Int -> [Int]
solve xs k = go 0 Seq.empty
where
n = length xs
arr = listArray (0, n - 1) xs
-- the deque holds indices; their values fall from front to back
go i dq
| i == n = []
| otherwise =
let dropBack d = case viewr d of
rest :> j | arr ! j <= arr ! i -> dropBack rest
_ -> d
pushed = dropBack dq |> i
live = case viewl pushed of
j :< rest | j <= i - k -> rest -- the front has left the window
_ -> pushed
front = case viewl live of
j :< _ -> arr ! j
EmptyL -> 0
in if i >= k - 1 then front : go (i + 1) live else go (i + 1) live
Variants to try
- Minimum of every window: the same deque with the comparison reversed.
- Shortest subarray with sum at least K when values can be negative: a deque of prefix sums, kept increasing, gives O(n).
- Maximum of each window in a stream where k changes: a two-stack queue, each stack carrying its running maximum, handles push and pop in amortised O(1).
- Constrained subsequence sum: dynamic programming whose transition needs the maximum of the last k states, which is exactly this deque.
Next problems
Shortest window containing every letter
Count what the window still needs; extend on the right until nothing is missing, then cut from the left.
Open the problem → Medium15 minLongest run of distinct characters
The window that grows on the right and shrinks on the left, and the invariant that makes it correct.
Open the problem → Medium15 minShortest subarray with a sum at least the target
A window that grows on the right until it qualifies, then shrinks on the left while it still does.
Open the problem →Write a solution and run it against the real test table.
Keep going
Shortest window containing every letter
Count what the window still needs; extend on the right until nothing is missing, then cut from the left.
Open the problem → Medium15 minLongest run of distinct characters
The window that grows on the right and shrinks on the left, and the invariant that makes it correct.
Open the problem → Medium15 minShortest subarray with a sum at least the target
A window that grows on the right until it qualifies, then shrinks on the left while it still does.
Open the problem → Sliding windowThe pattern behind it
A window that grows on the right, shrinks on the left, and holds an invariant at all times.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.