Problem overview
You get an IntArray nums and a window size k. A window of k neighbouring values starts at the left end and slides one position to the right at a time until it reaches the right end. The method returns an IntArray with the largest value of every window, from left to right. An array of n values has n - k + 1 windows.
For nums = [1, 3, -1, -3, 5, 3, 6, 7] and k = 3 the answer is [3, 3, 5, 5, 6, 7]. For [7, 2, 4] and k = 2 it is [7, 4]: once the 7 has left the window, the 4 is the largest value.
Approach
The brute force scans the k values of each window to find the maximum. That takes O(n · k) time, and with 100,000 values and a large k it is too slow. Two windows next to each other share k - 1 values, so most of that work is repeated.
The lesson keeps a deque (a queue that works at both ends) of indexes, called candidates. It holds the values that can still become the maximum of a window, and the values they point to always decrease from the front to the back. Three rules keep it that way:
- Old entries leave at the front. The oldest index sits at the front. When it falls outside the window, it is removed.
- Weak entries leave at the back. When a new value
nums[i]arrives, every value at the back that is not bigger than it is removed. Such a value is older thannums[i], so it leaves the window first, and as long as it stays,nums[i]is there too and at least as large. It can never be a maximum again. - The front is the answer. The values decrease from front to back, so the front is the largest value of the window.
Each index is added once and removed at most once, so the total work is O(n), however big k is. Storing indexes, not values, is what lets us tell when an entry has slid out of the window.
Step by step
- The deque. Create
val candidates = ArrayDeque<Int>(). Kotlin'sArrayDequeadds and removes at both ends in constant time. - The result. Create
val result = IntArray(nums.size - k + 1), one slot for each window. - The loop. Loop
iovernums.indices. Each turn,iis the right end of the window: the window is the k values that end at indexi. - Drop the front if it left the window. The window starts at
i - k + 1, so an index ofi - kor less is outside it. Indexes arrive in increasing order, and only one index leaves per turn, so a singleifis enough: whencandidatesis not empty andcandidates.first() <= i - k, callremoveFirst(). - Drop weak entries from the back. A
whileloop runs whilecandidatesis not empty andnums[candidates.last()] <= nums[i]. - Remove them. Inside that loop, call
candidates.removeLast(). - Add the new index with
candidates.addLast(i). The values left in the deque are all bigger thannums[i], so they still decrease. - Record the maximum. Once the window is full,
i >= k - 1, the front is its maximum:result[i - k + 1] = nums[candidates.first()]. The window that ends atistarts ati - k + 1, which is also its place inresult. - Return
resultafter the loop.
The solution in Kotlin
class Solution {
fun maxSlidingWindow(nums: IntArray, k: Int): IntArray {
val candidates = ArrayDeque<Int>()
val result = IntArray(nums.size - k + 1)
for (i in nums.indices) {
if (candidates.isNotEmpty() && candidates.first() <= i - k) candidates.removeFirst()
while (candidates.isNotEmpty() && nums[candidates.last()] <= nums[i]) {
candidates.removeLast()
}
candidates.addLast(i)
if (i >= k - 1) result[i - k + 1] = nums[candidates.first()]
}
return result
}
}
Kotlin details:
ArrayDequehere iskotlin.collections.ArrayDeque, which Kotlin imports automatically. It is notjava.util.ArrayDeque. It hasfirst(),last(),addLast,removeFirst()andremoveLast().first()andlast()throw an exception on an empty deque, which is why each use is guarded byisNotEmpty(). The&&stops at the first false part, so the second half is not evaluated for an empty deque.ArrayDeque<Int>holds boxedIntegerobjects, whilenumsandresultare primitiveIntArrays. That is fine at this size.removeLast()returns the removed value; the loop ignores it.- The result is an
IntArray, so print it withtoList()orcontentToString()to see the numbers.
A trace
For nums = [1, 3, -1, -3, 5, 3, 6, 7] and k = 3, printing the deque after each turn gives the indexes, the values they point to, and the window maximum (a dash while the window is not full yet):
i=0 nums[i]=1 deque(indexes)=[0] values=[1] max=-
i=1 nums[i]=3 deque(indexes)=[1] values=[3] max=-
i=2 nums[i]=-1 deque(indexes)=[1, 2] values=[3, -1] max=3
i=3 nums[i]=-3 deque(indexes)=[1, 2, 3] values=[3, -1, -3] max=3
i=4 nums[i]=5 deque(indexes)=[4] values=[5] max=5
i=5 nums[i]=3 deque(indexes)=[4, 5] values=[5, 3] max=5
i=6 nums[i]=6 deque(indexes)=[6] values=[6] max=6
i=7 nums[i]=7 deque(indexes)=[7] values=[7] max=7
At i=1 the 3 removes the 1, because it is bigger and arrived later. At i=4 the 5 removes everything: index 1 has just left the window, and the 5 beats the -1 and -3. The deque is always decreasing, and its front is the maximum.
Complexity
Time: O(n). The outer loop runs n times. The inner while loop looks like extra work per turn, but every index is added once and removed at most once, so all the removals together are at most n.
Space: O(n). The result has n - k + 1 entries, and the deque holds at most k indexes.
Edge cases and common mistakes
- Using
<instead of<=in the back check. Equal values would then stay in the deque. With<=an equal older value is dropped, which is safe because the newer one lasts longer. For[4, 4, 4, 4]andk = 2the deque holds just one index after every turn, and the result is[4, 4, 4]. - Storing values instead of indexes. You could not tell when the front has left the window.
- Using
i - k + 1for the leaving test. The test isfirst() <= i - k: the window starts ati - k + 1, and anything before that is out. - Writing results too early. Before
ireachesk - 1the window is not full, and nothing is recorded; the first answer goes toresult[0]. - Empty-deque calls. Calling
first()orlast()without theisNotEmpty()check throws an exception. - Special sizes. With
k = 1every value is its own window:[3, 1, 2]gives[3, 1, 2]. Withk = nums.sizethere is one window:[9, 1, 8]withk = 3gives[9].
Testing
These calls were run against the solution above:
nums | k | Result |
|---|---|---|
[1, 3, -1, -3, 5, 3, 6, 7] | 3 | [3, 3, 5, 5, 6, 7] |
[1] | 1 | [1] |
[7, 2, 4] | 2 | [7, 4] |
[5, 4, 3, 2, 1] | 3 | [5, 4, 3] |
[1, 2, 3, 4, 5] | 3 | [3, 4, 5] |
[4, 4, 4, 4] | 2 | [4, 4, 4] |
[3, 1, 2] | 1 | [3, 1, 2] |
[9, 1, 8] | 3 | [9] |
