Start learning free

LESSON 399 OF 573 · SLIDING WINDOW

Sliding Window Maximum in Scala: O(n) with a Monotonic Deque

Practice O(n) Read the full explanation

You are given an array of integers nums and an integer k. A window of size k starts at the left end of nums and slides to the right one position at a time, until it reaches the right end. At each position, the window covers k neighbouring values.

Return an array with the largest value of each window, from left to right.

 

Example 1:

Input: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
Output: [3, 3, 5, 5, 6, 7]
Explanation:
Window position                Max
---------------                ---
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

Example 2:

Input: nums = [1], k = 1
Output: [1]
Explanation: There is one window, and it holds the only value.

Example 3:

Input: nums = [7, 2, 4], k = 2
Output: [7, 4]
Explanation: The windows are [7, 2] and [2, 4]. Once 7 has left the window, 4 is the largest value.

 

Constraints:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length

Note:

Your algorithm should run in O(n) time, where n is the length of nums, and use O(n) extra space. Finding each window's maximum from scratch takes O(n · k) time, too slow for the largest tests.

STEP 1 OF 9
    Ctrl + Enter to check

    SCALA · SLIDING WINDOW

    Sliding Window Maximum in Scala, explained

    Problem overview

    The method maxSlidingWindow receives an Array[Int] named nums and a window size k. The window starts at the left end of nums and slides right one position at a time until it reaches the end. At each position it covers k neighbouring values. The method returns an array with the largest value of each window, from left to right.

    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 their maxima are [3, 3, 5, 5, 6, 7]. An array of length n has n - k + 1 windows, so the result has that length. For [7, 2, 4] with k = 2 the answer is [7, 4]: once the 7 has left the window, 4 is the largest value.

    The lesson asks for O(n) time and O(n) extra space.

    Approach: keep only the values that can still win

    The brute force scans all k values of every window, which costs O(n * k). With 100,000 values and a large k, that is far too slow.

    Notice that many values in a window can never be the answer. Suppose a 3 sits before a 5 inside the window. The 3 leaves the window first, because it is further left, and for as long as it stays, the 5 is there too and is bigger. So the 3 can never be a maximum again and can be forgotten.

    That leads to a monotonic deque: a double-ended queue holding the indexes of the candidate values, with those values decreasing from front to back. Two rules keep it correct:

    • When a new value arrives, remove from the back every candidate whose value is less than or equal to it. They can never win again. Then add the new index at the back.
    • When the front index has slid out of the window, remove it from the front.

    The front is then always the maximum of the current window: it is the largest candidate, and every window value that isn't in the deque was dropped because something at least as big sat to its right. Every index enters the deque once and leaves it at most once, so the whole pass is O(n), whatever k is.

    The deque while processing the first example, with the values it holds:

    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=3 the front index 1 is still inside the window that starts at index 1, and the maximum is 3. At i=4 the incoming 5 clears the whole deque. Once the window is full (from i=2), the value at the front index is the window's maximum.

    Step by step

    1. Create the deque. mutable.ArrayDeque[Int]() in a val named candidates adds and removes at both ends in constant time. It stores indexes, not values: an index tells when an entry has left the window, and nums(index) still gives the value.
    2. Create the result array with Array.fill(nums.length - k + 1)(0), one slot per window.
    3. Visit every value with for (i <- nums.indices). On each turn, i is the right end of the window.
    4. Drop the front if it left the window. The window now starts at i - k + 1, so a front index of i - k or less is outside it. Indexes are in increasing order and only one can expire per turn, so a single if is enough.
    5. Drop weaker candidates from the back. A while loop runs as long as the deque is not empty and nums(candidates.last) <= nums(i).
    6. Remove the back inside that loop with removeLast().
    7. Add the new index at the back with append(i). It is smaller than every value left in the deque, so the values still decrease. It may become the maximum later, when the bigger values ahead of it expire.
    8. Record the maximum once the window is full. When i >= k - 1, store nums(candidates.head) in result(i - k + 1). The window ending at i starts at i - k + 1, which is also its place in the result.
    9. Return result as the last expression.

    The solution in Scala

    class Solution {
        def maxSlidingWindow(nums: Array[Int], k: Int): Array[Int] = {
            val candidates = mutable.ArrayDeque[Int]()
            val result = Array.fill(nums.length - k + 1)(0)
            for (i <- nums.indices) {
                if (candidates.nonEmpty && candidates.head <= i - k) candidates.removeHead()
                while (candidates.nonEmpty && nums(candidates.last) <= nums(i)) {
                    candidates.removeLast()
                }
                candidates.append(i)
                if (i >= k - 1) result(i - k + 1) = nums(candidates.head)
            }
            result
        }
    }
    

    The Scala details worth a closer look:

    • The import. The lesson's editor already makes mutable available. In a file of your own, add import scala.collection.mutable at the top.
    • ArrayDeque is the right tool. The immutable List can't remove from its back cheaply, and a plain ArrayBuffer removes from the front in linear time. mutable.ArrayDeque does head, last, removeHead(), removeLast() and append in amortised constant time.
    • nonEmpty guards head and last. Calling head or last on an empty deque throws an exception. The && operator stops at the first false part, so the second condition is only evaluated when the deque has something in it.
    • Methods called with (). removeHead() and removeLast() change the deque and return the removed element. The solution ignores that return value.
    • if with one statement, no braces. The one-line forms for the front check and for the result store are fine, as long as each controls a single statement.
    • Assigning into an array. result(i - k + 1) = ... updates one element in place. The array is a val, but its contents are mutable.
    • The last expression returns. The method ends with result, and no return keyword is needed.

    Complexity

    Time: O(n). Each index is appended once and removed at most once, from either the front or the back. The inner while loop looks costly, but the total number of removals across the whole run is at most n.

    Space: O(n). The result holds n - k + 1 values, and the deque holds at most k indexes. The deque's size depends on k, never beyond n.

    Edge cases and common mistakes

    • One element. [1] with k = 1 gives [1].
    • k = 1. Every window is a single value, so [1, 2, 3] gives [1, 2, 3].
    • k equal to the length. There is one window. [9, 8, 7] with k = 3 gives [9].
    • Equal values. [5, 5, 5, 5] with k = 3 gives [5, 5]. Removing candidates that are equal to the new value (<=, not <) is safe, because the newer copy outlives the older one.
    • Decreasing and increasing input. [4, 3, 2, 1] with k = 2 gives [4, 3, 2], where the deque keeps growing and the front is removed as it expires. [1, 2, 3, 4] gives [2, 3, 4], where each new value clears the deque.
    • Storing values instead of indexes. Without indexes, the code can't tell when the front has left the window.
    • A wrong expiry test. Using candidates.head < i - k keeps an index that has just left, so a window can report a maximum from outside itself.
    • Writing results too early. Before i reaches k - 1 the window isn't full yet. Storing then would use a negative result index and crash.

    Testing

    • nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 gives [3, 3, 5, 5, 6, 7].
    • nums = [1], k = 1 gives [1].
    • nums = [7, 2, 4], k = 2 gives [7, 4].
    • nums = [4, 3, 2, 1], k = 2 gives [4, 3, 2].
    • nums = [1, 2, 3, 4], k = 2 gives [2, 3, 4].
    • nums = [5, 5, 5, 5], k = 3 gives [5, 5].
    • nums = [9, 8, 7], k = 3 gives [9].
    Keep going with Scala500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free