Problem overview
The function max_sliding_window receives a vector nums and a window size k. A window of k neighbouring values starts at the left end of nums and slides right one position at a time. The function returns the largest value of each window, from left to right, so the result has nums.len() - k + 1 entries.
For nums = [1, 3, -1, -3, 5, 3, 6, 7] and k = 3, the windows are [1, 3, -1], [3, -1, -3], [-1, -3, 5], [-3, 5, 3], [5, 3, 6] and [3, 6, 7]. Their maxima are [3, 3, 5, 5, 6, 7]. For [7, 2, 4] and k = 2 the answer is [7, 4]: once the 7 has left the window, the 4 is the largest value.
The lesson asks for O(n) time. Scanning each window for its maximum costs O(n · k), which is too slow when both are in the tens of thousands.
Approach: a deque of candidates
The key observation is that most values can never be the answer again. Suppose a value a sits to the left of a value b and a <= b. Any window that contains a and also reaches b has b in it. And a leaves the window before b does, because it is further left. So as soon as b arrives, a is useless.
The code keeps a deque, candidates, of the indexes of the values that can still be a window's maximum. Three rules hold it together:
- The values behind the indexes decrease from front to back. When a new value arrives, every value at the back that is less than or equal to it is removed first.
- The new index is then pushed at the back.
- The front index is dropped when it has slid out of the window.
The values decrease from front to back, and every dropped value was no bigger than one that stayed, so the front is always the maximum of the current window. Each index enters the deque once and leaves it at most once, so the work adds up to O(n) however big k is.
The deque holds indexes, not values. The index tells you both the value (nums[index]) and whether the entry is still inside the window. Here is the deque for nums = [1, 3, -1, -3, 5, 3, 6, 7] with k = 3 after each step:
i=0 value=1 deque indexes=[0] values=[1]
i=1 value=3 deque indexes=[1] values=[3]
i=2 value=-1 deque indexes=[1, 2] values=[3, -1]
i=3 value=-3 deque indexes=[1, 2, 3] values=[3, -1, -3]
i=4 value=5 deque indexes=[4] values=[5]
i=5 value=3 deque indexes=[4, 5] values=[5, 3]
i=6 value=6 deque indexes=[6] values=[6]
i=7 value=7 deque indexes=[7] values=[7]
At i = 4 the 5 removes all of 3, -1 and -3 from the back, because each is smaller. From i = 2 on the window is full, and the front's value is the maximum: 3, 3, 5, 5, 6, 7.
Step by step
- Make
kausize. It arrives as ani32but is compared with indexes. A newlet k = k as usize;hides the old one for the rest of the function. - Create the deque: a mutable
VecDeque<usize>namedcandidatesfromVecDeque::new(). It adds and removes at both ends in constant time. - Create
resultwithVec::new(). Rust infers its element type from what is pushed later. - Loop over every index
iin0..nums.len(). Each turniis the window's right end. - Drop the front if it left the window. The window is the
kvalues ending ati, so an index ofi - kor less is outside it. The code testsfront + k <= iinstead offront <= i - k, because ausizecan't hold a negativei - kwhilei < k. - Drop weaker values from the back with a
whileloop, as long as the deque is not empty andnums[back] <= nums[i]. - Remove the back inside that loop with
pop_back(). - Add
iat the back withpush_back(i). It is smaller than everything left in the deque, so the order stays decreasing. - Record the maximum once the window is full. When
i >= k - 1the window holdskvalues, and the front's value goes intoresult. - Return
resultwithout a semicolon.
The solution in Rust
use std::collections::VecDeque;
impl Solution {
fn max_sliding_window(nums: Vec<i32>, k: i32) -> Vec<i32> {
let k = k as usize;
let mut candidates: VecDeque<usize> = VecDeque::new();
let mut result = Vec::new();
for i in 0..nums.len() {
if !candidates.is_empty() && *candidates.front().unwrap() + k <= i {
candidates.pop_front();
}
while !candidates.is_empty() && nums[*candidates.back().unwrap()] <= nums[i] {
candidates.pop_back();
}
candidates.push_back(i);
if i >= k - 1 {
result.push(nums[*candidates.front().unwrap()]);
}
}
result
}
}
The Rust details worth a closer look:
- Shadowing.
let k = k as usize;declares a new variable with the same name. The oldi32kcan no longer be named, which avoids mixing the two types. The cast is safe becausekis at least 1. front()andback()return options. They give anOption<&usize>,Nonewhen the deque is empty.unwrap()takes out the reference, and*reads the number through it. The!candidates.is_empty() &&before each call makes theunwrapsafe, since&&stops at the first false part.pop_front()andpop_back()return the removed entry as anOption. The code doesn't need it, so it drops it.- Indexing with a dereferenced index.
nums[*candidates.back().unwrap()]reads the value stored under the index at the back. The index has to be ausizeto index a vector, which is why the deque holdsusize. k - 1on ausize. It is safe becausekis at least 1. Withk = 0it would underflow: a panic in a debug build and a wrap-around in an optimized one.- The
useline.VecDequelives instd::collections, and the lesson's code starts with theuseline that brings it in.
Complexity
Time: O(n). The outer loop runs n times. Inside it, the if removes at most one entry, and the while loop removes entries that were each pushed exactly once. Over the whole run, at most n entries are pushed and at most n are popped, so all the pops together cost O(n).
Space: O(n). The result holds n - k + 1 values, and the deque never holds more than k indexes.
Edge cases and common mistakes
k = 1. Every window is a single value, so the result isnumsitself. For[1, 2, 3, 4, 5]it is[1, 2, 3, 4, 5].kequal to the length. There is one window. For[5, 4, 3, 2, 1]withk = 5the result is[5].- Equal values.
[4, 4, 4, 4]withk = 2returns[4, 4, 4]. The<=in thewhilecondition drops an equal value from the back, keeping the newer copy, which leaves the window later. Equal values are never both needed: the newer one stays in the window at least as long. - A decreasing input. Nothing is ever removed from the back, so the deque fills up to
kentries and only the front check removes anything.[9, 8, 7, 6]withk = 2returns[9, 8, 7]. - Storing values instead of indexes. Then there is no way to tell when the front has left the window.
- Writing
i - kwith unsigned numbers. Whilei < kit underflows: a panic in a debug build, a huge wrapped number in an optimized one. The lesson's formfront + k <= iavoids subtraction. - Pushing results too early. Before index
k - 1the window is not yet a full window of the problem.
Testing
nums = [1, 3, -1, -3, 5, 3, 6, 7],k = 3gives[3, 3, 5, 5, 6, 7].nums = [1],k = 1gives[1].nums = [7, 2, 4],k = 2gives[7, 4].nums = [4, 4, 4, 4],k = 2gives[4, 4, 4].nums = [5, 4, 3, 2, 1],k = 5gives[5].nums = [1, 2, 3, 4, 5],k = 1gives[1, 2, 3, 4, 5].nums = [9, 8, 7, 6],k = 2gives[9, 8, 7].
