Problem overview
The method maxSlidingWindow receives a list nums and an integer k. A window of k neighbouring values starts at the left end and moves right one position at a time. The method returns the largest value of every window, from left to right, so a list of n - k + 1 numbers.
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], and the result is [3, 3, 5, 5, 6, 7]. With nums = [7, 2, 4] and k = 2 the result is [7, 4]: once the 7 has left the window, the 4 is the largest value.
The lesson asks for O(n) time and O(n) extra space. Calling max on every window costs O(n · k), which is too slow when both are large.
Approach: keep only the values that can still win
When a new value nums[i] enters the window, any earlier value in the window that is not bigger can never be the maximum again. It sits to the left, so it leaves the window first, and for as long as it stays, nums[i] is there too and is at least as big. Such values can be thrown away at once.
That leaves a list of candidates: values that are still in the window and are strictly bigger than everything that came after them. Read from the oldest to the newest, they decrease. The first candidate is therefore always the maximum of the window.
The candidates live in a deque (a double-ended queue), which adds and removes at both ends in constant time. The deque stores indexes, not values, so the code can tell when the oldest candidate has slid out of the window, and it reads the values through nums. Each turn does three things:
- drop the front index if it has left the window;
- drop indexes from the back while their values are not bigger than
nums[i]; - add
iat the back, and once the window is full, reportnums[candidates[0]].
Each index enters the deque once and leaves it at most once, so all the work adds up to O(n), however large k is.
Here is the first example, nums = [1, 3, -1, -3, 5, 3, 6, 7] with k = 3. The table shows the deque's indexes and values after each turn:
i | nums[i] | indexes in the deque | their values | reported |
|---|---|---|---|---|
| 0 | 1 | [0] | [1] | (window not full) |
| 1 | 3 | [1] | [3] | (window not full) |
| 2 | -1 | [1, 2] | [3, -1] | 3 |
| 3 | -3 | [1, 2, 3] | [3, -1, -3] | 3 |
| 4 | 5 | [4] | [5] | 5 |
| 5 | 3 | [4, 5] | [5, 3] | 5 |
| 6 | 6 | [6] | [6] | 6 |
| 7 | 7 | [7] | [7] | 7 |
At i = 4 the value 5 beats everything in the deque, so all three entries are removed. The reported values are [3, 3, 5, 5, 6, 7].
Step by step
- Create the empty deque
candidateswithdeque(). It holds indexes whose values decrease from front to back. - Create the empty list
resultfor the maxima. - Loop over every index
i. Each turn,iis the right end of the window: thekvalues that end ati. - Drop the front if it expired. The window now starts at
i - k + 1, so an index ofi - kor less is outside it. Indexes arrive in increasing order, so the oldest is at the front, and one check per turn is enough because at most one index expires per turn. - Write the
whileloop that runs while the deque is not empty and the value at its back,nums[candidates[-1]], is<= nums[i]. - Inside it, call
candidates.pop(), which removes from the back. - Append
iwithcandidates.append(i). Every value left in the deque is bigger thannums[i], so the order stays decreasing. - If the window is full,
i >= k - 1, appendnums[candidates[0]]toresult. Before that the window holds fewer thankvalues. - Return
result.
The solution in Python
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
candidates = deque()
result = []
for i in range(len(nums)):
if candidates and candidates[0] <= i - k:
candidates.popleft()
while candidates and nums[candidates[-1]] <= nums[i]:
candidates.pop()
candidates.append(i)
if i >= k - 1:
result.append(nums[candidates[0]])
return result
Python details worth knowing:
- The deque.
dequecomes from thecollectionsmodule; in a file of your own, addfrom collections import deque. Itsappendandpopwork at the right end,popleftat the left end, andcandidates[0]andcandidates[-1]read the two ends. A plain list would do the right end well, butlist.pop(0)shifts every remaining item, which is why a deque is the right tool. - Truthiness.
candidates and ...uses the fact that an empty deque counts as false, so the right-hand side is only evaluated when the deque has something in it. That prevents anIndexErroroncandidates[0]orcandidates[-1]. - Indexes, not values. The expression
nums[candidates[-1]]first reads the index at the back, then looks up its value. - Negative indexes.
candidates[-1]is the last element, the back of the deque.
Complexity
Time: O(n). The outer loop runs n times. The inner while loop looks like a loop within a loop, but every pass of it removes an index that was added earlier, and each index is added exactly once. So all the removals together cost at most n steps, and the whole function stays linear regardless of k.
Space: O(n). The deque holds at most k indexes (for a list in decreasing order, it fills up to the window size), and the result holds n - k + 1 numbers. Both are bounded by n.
Edge cases and common mistakes
- Forgetting to expire the front. Without the
popleftcheck,[7, 2, 4]withk = 2returns[7, 7]: the 7 stays in front long after it has left the window. - Window of one. With
k = 1every window is a single value, so the result equalsnums. For[1, -1]the method returns[1, -1]. - Window as big as the list. With
k = len(nums)there is one window. For[9, 8, 7]andk = 3the result is[9]. - Decreasing and increasing lists. For
[4, 3, 2, 1]withk = 2the deque grows to the window size and the result is[4, 3, 2]. For[1, 2, 3, 4]each new value clears the deque and the result is[2, 3, 4]. - Equal values. With
<=in thewhiletest, an equal value at the back is replaced by the newer one, which stays in the window longer. For[5, 5, 5]andk = 2the result is[5, 5]. - Negative numbers. Nothing special is needed. For
[-5, -3, -4]andk = 2the result is[-3, -3]. - Reporting too early. Without the
i >= k - 1test, the method would append a maximum for the firstk - 1partial windows, and the result would be too long.
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, 3, 2, 1],k = 2gives[4, 3, 2].nums = [1, 2, 3, 4],k = 2gives[2, 3, 4].nums = [5, 5, 5],k = 2gives[5, 5].nums = [-5, -3, -4],k = 2gives[-3, -3].
