Solving the Sliding Maximum Window Problem in O(n) Time

An optimal solution to the sliding maximum window interview question

Photo by Ryan Quintal on Unsplash

Hello, fellow coffee junkies! Today I’m walking you through one of my favorite problems: the sliding maximum window.

Outwardly, this may look like a fairly simple problem, and it is, with a brute force approach, you could be done in minutes or if you have great brain and finger speed co-ordination like I do, seconds.

I’ll show you the problem statement in a moment — pause reading and try to figure it out on your own if you like. Hint: A Deque is your best bet.

Check out this tutorial on Deques if you need a refresher or help to implement one.

Problem Statement

For every k continuous numbers in an array, print the largest number. This challenge is easy to brute-force, but try to find the optimal solution.

Simple Solution: O(nk)

Better Solution: O(n log k)

Optimal Solution: O(n)

Example

Here are some example test cases you could run against your solution:

sliding_maximum(3, [1, 3, 5, 7, 9, 2])

# # => [5, 7, 9, 9]

sliding_maximum(4, [9, 3, 5, 1, 7, 10])

# # # => [9, 7, 10]

Pretty simple, right? Here’s an example of a solution most people will find within a few seconds:

Brute force solution

This may seem OK if you have a small data set and don’t care much for time complexity. In fact, in terms of time complexity, this solution is the Joker from Batman: let it go and it will return to bite you in the ass as it gives you O(nk).

On to the reason for this article.

This problem took me almost 12 hours to solve but those 12 hours were worth it because the new knowledge I gained opened a lot of mental doors for me in how I view problems these days.

First, let’s break the problem down and think about the first piece. We need to find a way to keep track of the maximum value in a particular window while at the same time, keeping track of potential maximum values for our next window in our current window. It’s OK — read it once more if you’re confused.

Thinking about this, we begin to see why you’d want a deque — you can perform O(1) operations on the back or the front of the data set.

Let’s get the basic stuff out of the way by splitting our function into three parts:

Initialize the deque and result vector

The loop to go over the array

Return the result

Split the sliding maximum function into three main parts:

If you need help setting up your deque class, please take a look at my previous tutorial on deques.

Next, we need a loop to run through the array. The for loop is my favorite in this situation, but be sure to experiment with whatever loop you are most comfortable with.

Now the real fun begins!

Everything we write from this point, except the function definitions I termed as external declarations, should go inside your loop.

The first thing we want to think about is adding an element to our deque if it is empty, so we can have something to compare to the elements in the first window. We add the first element in the array to the deque, since the deque is empty:

This is just a simple one-liner to push the first element into the deque when we first enter the loop.

Note: is_empty? is a method in the deque class. Check out this tutorial for more information on this.

Next, we want to compare the current max to all other numbers in a given window so we first need to obtain the max.

I also declared an external get_max function. Let’s see what it does.

It returns the element in front of the deque (the element in front is always our max for a given window).

Now, for subsequent numbers in a window, we compare them to the current maximum number (the number stored in our max variable). If the current max is less than the number we arrive at, we remove the old max and set the current number as the max.

Let’s breakdown the set_max method:

This takes the deque and the max as arguments and pushes the max to the front of the deque.

Let’s take a look at what remove_max does. It takes the deque as an argument and pops the front (removes the first element, which is our max):

There are a few other things we may want to think about. What happens if there is a potential maximum number for the next window in our current window? Currently, our algorithm will skip it if it is less than the maximum in our current window, but we can’t let that slide — that could be our next winner!

Let’s store these potential winners in the back of the deque:

Finally, we may want to ask “what if we carry over a maximum value from one window into the next?” Our algorithm could count that value that we carried over as the next maximum and we can’t really blame it because an algorithm is only as smart as the person who wrote it.

We want to prevent this and make our algorithm smarter. The first thing is to decide how to find out if an element is out of the window. Take a look at the illustration below for an explanation of this:

We’ve identified the element that is out of bounds, the next step is to remove it from the max position of our deque. Our remove_max method is the geek for the job.

Note: Remember to add this snippet before the block that checks for potential maximum values

Now, we can get our new max value. If we’re at an index that is greater than or equal to our current window, we can say that’s the max value for the given window and then append it to our result vector.

Thanks for coming along on this ride!

Here is a link to the code for this tutorial.