Solution: Sliding Window Maximum

Statement

Given an integer array nums, find the maximum values in all the contiguous subarrays (windows) of size w.

Constraints:

  • 11 \leq nums.length \leq 10310^3
  • 104-10^4 \leq nums[i] \leq 10410^4
  • 11 \leq w \leq nums.length

Solution

So far, you’ve probably brainstormed some approaches on how to solve this problem. Let’s explore some of these approaches and figure out which one to follow while considering time complexity and any implementation constraints.

Naive approach

A naive approach is to slide the window over the input array and find the maximum in each window separately. We iterate over the input array, calculating the maximum element in each window linearly, and then adding it to the output array. In each subsequent iteration, we update the current window by removing the first element from the current window and adding the incoming element of the input array. Once we are done iterating the input array, we return the output array, containing the maximums of all (nw+1)(n-w+1) windows.

The time complexity of this approach is O(n×w)O(n \times w). Since we only iterate over the input array once to find all the windows, the time complexity of this part will be O(n)O(n). Furthermore, removing the first element, adding the incoming element, and finding the maximum element in the window take O(w)O(w) time. The space complexity of this approach is O(w)O(w), since we maintain an array for the window.

Optimized approach using sliding window

Our first solution uses the sliding window technique to solve the problem. However, there is much room for improvement. In the following section, we will gradually optimize our naive approach to reduce the time complexity to O(n)O(n).

Note: In the following section, we will gradually build the solution. Alternatively, you can skip straight to just the code.

Step-by-step solution construction

Firstly, instead of adding values to currentWindow, we use their indexes. By doing this, we can easily check which index has fallen out of the current window and remove it.

Secondly, we process the elements of the first window as follows:

  • Every time we add a new index to currentWindow, we clean it up, i.e., we iterate backward over currentWindow, starting from the end, and remove all the indexes whose corresponding values in the input array are smaller than or equal to the new element in the window. Let’s understand how this step benefits us. Whenever a new element enters the window, all the previous elements smaller than the current element can no longer be the maximum in the current or any subsequent windows containing the new element. This is because all the subsequent windows holding the indexes of the removed elements will also include the new, bigger element. Since this new element is bigger than those elements, keeping those smaller elements in currentWindow is unnecessary.

  • A key detail to note here is that we perform the clean-up step starting with the second element added to the first window. As a result, even in the first window, we will have excluded all elements smaller than the maximum of that window that occurs before it in the input array.

  • Let’s understand this using an initial frame of five elements, [2,4,5,3,1][2, 4, 5, 3, 1]. When the index of 44 is added to currentWindow, it causes the index of 22 to be deleted. The addition of the index of 55 causes the index of 44 to be deleted. However, the addition of the indexes of 33 and 11 does not trigger any deletions. At the end of this clean-up process, currentWindow contains the indexes, [2,3,4][2, 3, 4], corresponding to the values, [5,3,1][5, 3, 1].

  • Once we have cleaned up the first window, the remaining indexes in currentWindow will be stored in the descending order of their values. Now, let’s imagine the other possibility where the first frame actually contains [2,4,5,1,3][2, 4, 5, 1, 3]. Here, the addition of the index of 11 does not trigger any deletion, but 33 does cause the index of 11 to be deleted. currentWindow now holds the indexes [2,4][2, 4], corresponding to the values [5,3][5, 3], which are sorted in descending order. Since we’ve examined both possibilities (either the elements in nums after element 55 are in descending order, or they aren’t), we can be sure that, after the clean-up process, the index of the maximum element in the first window will always be stored at the start of currentWindow.

  • We append the value corresponding to the index at the start of currentWindow to our output array.

Next, we iterate over the remaining input array. For each element, we perform the clean-up step as we did for the elements of the first window.

One difference when processing the second and all subsequent windows, compared to the processing of the first window, is an additional check that is carried out before we append the current element in nums to currentWindow. We check whether the first index in currentWindow is still a part of the current window. If not, we remove the first index from currentWindow.

Lastly, when the entire input array has been processed, one window at a time, we return the output array containing the maximum of each window.

Let’s understand this algorithm with the help of an illustration:

Press + to interact
canvasAnimation-image
1 of 12

Now, let’s analyze the algorithm in terms of complexity. We are still moving over the input array in O(n)O(n) time and using the sliding window approach to maintain currentWindow. Inside the loop, we append the new element to currentWindow, which takes O(1)O(1) time. The time complexity of the clean-up step is O(1)O(1), which is explained in detail in the complexity analysis of the final solution. Lastly, we remove the index that no longer falls in the current window. Since this element is removed from the start of the array, the time complexity of removing this index will be O(w)O(w). Therefore, the overall time complexity of this solution will be O(n×w)O(n \times w). The space complexity of this solution is O(w)O(w), since we maintain an array to store the indexes of significant elements from the current window.

import java.util.*;
import java.util.stream.*;
class SlidingWindowMaximum {
// function to clean up the window
public static ArrayList<Integer> cleanUp(int i, ArrayList<Integer> currentWindow, int[] nums) {
// remove all the indexes from currentWindow whose corresponding values are smaller than or equal to the current element
while (currentWindow.size() != 0 && nums[i] >= nums[currentWindow.get(currentWindow.size() - 1)]) {
System.out.println("\t\tAs nums[" + i + "] = " + nums[i] + " is greater than or equal to nums[" + currentWindow.get(currentWindow.size() - 1) + "] = " + nums[currentWindow.get(currentWindow.size() - 1)] + ",");
System.out.println("\t\tremove " + currentWindow.get(currentWindow.size() - 1) + " from currentWindow");
currentWindow.remove(currentWindow.size() - 1);
}
// returning the altered currentWindow
return currentWindow;
}
// function to find the maximum in all possible windows
public static int[] findMaxSlidingWindow(int[] nums, int w) {
// If the length of input array is 1, return the input array
if (nums.length == 1) {
return nums;
}
// initializing variables
int [] output = new int[nums.length - w + 1];
ArrayList<Integer> currentWindow = new ArrayList<Integer> ();
System.out.println("\n\tFinding the maximum in the first window:");
// iterate over the first w elements
for (int i = 0; i < w; i++) {
System.out.println("\tAdding nums[" + i + "] = " + nums[i] + " to the first window:\n\t\tInitial state of currentWindow: " + currentWindow);
// for every element, remove the previous smaller elements from currentWindow
currentWindow = SlidingWindowMaximum.cleanUp(i, currentWindow, nums);
// append the index of the current element to currentWindow
currentWindow.add(i);
System.out.println("\t\tFinal state of currentWindow: " + currentWindow);
}
// appending the maximum element of the current window to the output list
output[0] = nums[currentWindow.get(0)];
System.out.println("\n\tFinding the maximum in the remaining windows:");
// iterate over the remaining input list
for (int i = w; i < nums.length; i++) {
System.out.printf("\tAdding nums[%d] = %d to currentWindow:\n\t\tInitial state of currentWindow: %s\n", i, nums[i], currentWindow.toString());
// for every element, remove the previous smaller elements from currentWindow
cleanUp(i, currentWindow, nums);
// remove first index from the currentWindow if it has fallen out of the current window
if (!currentWindow.isEmpty() && currentWindow.get(0) <= (i - w)) {
System.out.printf("\t\tIndex %d has fallen out of the current window,\n", currentWindow.get(0));
System.out.println("\t\tso, remove it");
currentWindow.remove(0);
}
// append the index of the current element to currentWindow
System.out.printf("\t\tAppending %d to currentWindow\n", i);
currentWindow.add(i);
System.out.printf("\t\tFinal state of currentWindow: %s\n", currentWindow.toString());
// adding the maximum element of the current window to the output list
output[i - w + 1] = nums[currentWindow.get(0)];
}
// return the array containing output
return output;
}
// driver code
public static void main(String args[]) {
int windowSizes [] = {3, 3, 3, 3, 2, 4, 3, 2, 3, 6};
int [][] numLists = {
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
{1, 5, 8, 10, 10, 10, 12, 14, 15, 19, 19, 19, 17, 14, 13, 12, 12, 12, 14, 18, 22, 26, 26, 26, 28, 29, 30},
{10, 6, 9, -3, 23, -1, 34, 56, 67, -1, -4, -8, -2, 9, 10, 34, 67},
{4, 5, 6, 1, 2, 3},
{9, 5, 3, 1, 6, 3},
{2, 4, 6, 8, 10, 12, 14, 16},
{-1, -1, -2, -4, -6, -7},
{4, 4, 4, 4, 4, 4}
};
for (int i = 0; i < numLists.length; i++) {
System.out.println(i + 1 + ".\tInput array:\t" + Arrays.toString(numLists[i]));
System.out.println("\tWindow size:\t" + windowSizes[i]);
System.out.println("\n\tMaximum in each sliding window:\t" + Arrays.toString(findMaxSlidingWindow(numLists[i], windowSizes[i])));
Stream.generate(() -> "-").limit(100).forEach(System.out::print);
System.out.println();
}
}
}
First optimization with current window as an array

In the first optimization, we eliminated the additional creation of currentWindow, reducing the time complexity by a significant factor. Let’s see if we can further improve our solution.

At the moment, we are incurring two kinds of costs. The first cost is of iterating over the input array while sliding the window forward. No matter what approach we use, we cannot eliminate this cost. The second cost is of removing the first element from the array currentWindow. The array data structure will always impose this cost. Therefore, we need a data structure that removes elements in constant time both from the end and from the start.

The data structure that supports constant-time removals from both ends is a deque. A deque is a double-ended queue where the Push() and Pop() operations work in O(1)O(1) time at both ends. Therefore, just by maintaining currentWindow as a deque instead of as an array, we can reduce the time complexity of the above solution to O(n)O(n). This is the most optimized solution for solving this problem. The space complexity of the solution will still be O(w)O(w)—the maximum number of elements in the current window stored in the deque.

import java.util.*;
import java.util.stream.*;
class SlidingWindowMaximum {
// function to clean up the window
public static Deque<Integer> cleanUp(int i, Deque<Integer> currentWindow, int[] nums) {
// remove all the indexes from currentWindow whose corresponding values are smaller than or equal to the current element
while (currentWindow.size() != 0 && nums[i] >= nums[currentWindow.getLast()]) {
System.out.println("\t\tAs nums[" + i + "] = " + nums[i] + " is greater than or equal to nums[" + currentWindow.getLast() + "] = " + nums[currentWindow.getLast()] + ",");
System.out.println("\t\tremove " + currentWindow.getLast() + " from currentWindow");
currentWindow.removeLast();
}
// returning the altered currentWindow
return currentWindow;
}
// function to find the maximum in all possible windows
public static int[] findMaxSlidingWindow(int[] nums, int w) {
// If the length of input array is 1, return the input array
if (nums.length == 1) {
return nums;
}
// initializing variables
int [] output = new int[nums.length - w + 1];
Deque<Integer> currentWindow = new ArrayDeque<>();
System.out.println("\n\tFinding the maximum in the first window:");
// iterate over the first w elements
for (int i = 0; i < w; i++) {
System.out.println("\tAdding nums[" + i + "] = " + nums[i] + " to the first window:\n\t\tInitial state of currentWindow: " + currentWindow);
// for every element, remove the previous smaller elements from currentWindow
currentWindow = SlidingWindowMaximum.cleanUp(i, currentWindow, nums);
// append the index of the current element to currentWindow
currentWindow.add(i);
System.out.println("\t\tFinal state of currentWindow: " + currentWindow);
}
// appending the maximum element of the current window to the output list
output[0] = nums[currentWindow.getFirst()];
System.out.println("\n\tFinding the maximum in the remaining windows:");
// iterate over the remaining input list
for (int i = w; i < nums.length; i++) {
System.out.printf("\tAdding nums[%d] = %d to currentWindow:\n\t\tInitial state of currentWindow: %s\n", i, nums[i], currentWindow);
// for every element, remove the previous smaller elements from currentWindow
cleanUp(i, currentWindow, nums);
// remove first index from the currentWindow if it has fallen out of the current window
if (!currentWindow.isEmpty() && currentWindow.getFirst() <= (i - w)) {
System.out.printf("\t\tIndex %d has fallen out of the current window,\n", currentWindow.getFirst());
System.out.println("\t\tso, remove it");
currentWindow.removeFirst();
}
// append the index of the current element to currentWindow
System.out.printf("\t\tAppending %d to currentWindow\n", i);
currentWindow.add(i);
System.out.printf("\t\tFinal state of currentWindow: %s\n", currentWindow);
// adding the maximum element of the current window to the output list
output[i - w + 1] = nums[currentWindow.getFirst()];
}
// return the array containing output
return output;
}
// driver code
public static void main(String args[]) {
int windowSizes [] = {3, 3, 3, 3, 2, 4, 3, 2, 3, 6};
int [][] numLists = {
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
{1, 5, 8, 10, 10, 10, 12, 14, 15, 19, 19, 19, 17, 14, 13, 12, 12, 12, 14, 18, 22, 26, 26, 26, 28, 29, 30},
{10, 6, 9, -3, 23, -1, 34, 56, 67, -1, -4, -8, -2, 9, 10, 34, 67},
{4, 5, 6, 1, 2, 3},
{9, 5, 3, 1, 6, 3},
{2, 4, 6, 8, 10, 12, 14, 16},
{-1, -1, -2, -4, -6, -7},
{4, 4, 4, 4, 4, 4}
};
for (int i = 0; i < numLists.length; i++) {
System.out.println(i + 1 + ".\tInput array:\t" + Arrays.toString(numLists[i]));
System.out.println("\tWindow size:\t" + windowSizes[i]);
System.out.println("\n\tMaximum in each sliding window:\t" + Arrays.toString(findMaxSlidingWindow(numLists[i], windowSizes[i])));
Stream.generate(() -> "-").limit(100).forEach(System.out::print);
System.out.println();
}
}
}
Second optimization with current window as a deque

Just the code

Here’s the complete solution to this problem:

import java.util.*;
import java.util.stream.*;
class SlidingWindowMaximum {
// function to clean up the window
public static Deque<Integer> cleanUp(int i, Deque<Integer> currentWindow, int[] nums) {
while (currentWindow.size() != 0 && nums[i] >= nums[currentWindow.getLast()]) {
currentWindow.removeLast();
}
return currentWindow;
}
// function to find the maximum in all possible windows
public static int[] findMaxSlidingWindow(int[] nums, int w) {
if (nums.length == 1) {
return nums;
}
int [] output = new int[nums.length - w + 1];
Deque<Integer> currentWindow = new ArrayDeque<>();
for (int i = 0; i < w; i++) {
currentWindow = SlidingWindowMaximum.cleanUp(i, currentWindow, nums);
currentWindow.add(i);
}
output[0] = nums[currentWindow.getFirst()];
for (int i = w; i < nums.length; i++) {
cleanUp(i, currentWindow, nums);
if (!currentWindow.isEmpty() && currentWindow.getFirst() <= (i - w)) {
currentWindow.removeFirst();
}
currentWindow.add(i);
output[i - w + 1] = nums[currentWindow.getFirst()];
}
return output;
}
// driver code
public static void main(String args[]) {
int windowSizes [] = {3, 3, 3, 3, 2, 4, 3, 2, 3, 6};
int [][] numLists = {
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
{1, 5, 8, 10, 10, 10, 12, 14, 15, 19, 19, 19, 17, 14, 13, 12, 12, 12, 14, 18, 22, 26, 26, 26, 28, 29, 30},
{10, 6, 9, -3, 23, -1, 34, 56, 67, -1, -4, -8, -2, 9, 10, 34, 67},
{4, 5, 6, 1, 2, 3},
{9, 5, 3, 1, 6, 3},
{2, 4, 6, 8, 10, 12, 14, 16},
{-1, -1, -2, -4, -6, -7},
{4, 4, 4, 4, 4, 4}
};
for (int i = 0; i < numLists.length; i++) {
System.out.println(i + 1 + ".\tInput array:\t" + Arrays.toString(numLists[i]));
System.out.println("\tWindow size:\t" + windowSizes[i]);
System.out.println("\n\tMaximum in each sliding window:\t" + Arrays.toString(findMaxSlidingWindow(numLists[i], windowSizes[i])));
Stream.generate(() -> "-").limit(100).forEach(System.out::print);
System.out.println();
}
}
}
Sliding Window Maximum

Solution summary

To recap, the solution can be divided into the following parts:

  1. First, we check if the input array contains only one element. If it does, we directly return the input array because there is only one maximum element.

  2. Then, we process the first ww elements of the input array. We will use a deque to store the indexes of the candidate maximums of each window.

  3. For each element, we perform the clean-up step, removing the indexes of the elements from the deque whose values are smaller than or equal to the value of the element we are about to add to the deque. Then, we append the index of the new element to the back of the deque.

  4. After the first ww elements have been processed, we append the element whose index is present at the front of the deque to the output array, since it is the maximum in the first window.

  5. After finding the maximum in the first window, we iterate over the remaining input array. For each element, we repeat Step 3 as we did for the first ww elements.

  6. Additionally, in each iteration, before appending the index of the current element to the deque, we check if the first index in the deque has fallen out of the current window. If so, we remove it from the deque.

  7. Finally, we return the array containing the maximum elements of each window.

Time complexity

The input parameters of the function are an array of integers and an integer specifying the size of the window. In the discussion that follows, we will use nn to represent the size of the array of integers, and ww to represent the size of the window.

To get a clearer understanding of the time complexity of our solution, we need to consider the different ways in which the values in the input array change. The values in the array can be:

  1. Strictly increasing
  2. Strictly decreasing
  3. Constant
  4. Mixed, i.e, increasing, decreasing, constant, then decreasing again, then constant, then increasing, then constant and then decreasing

On the surface, we might expect our solution to take O(nw)O(n * w), but that would be no better than the naive solution. When we look closer at our solution, it comes down to figuring out how many times the clean-up loop actually runs. This is the loop that pops all elements from the deque that are smaller than the new element in the window.

Let’s consider the first case when the values in the array are strictly increasing. The first time the window moves forward, the new element is larger than all the other elements in the deque. Therefore, we have to perform the removeLast() operation ww times. Then, in all the subsequent steps, the removeLast() operation is performed only once, since the deque will only contain one element from this point onward. The number of subsequent steps is nwn-w. So, the complexity in this case is O(w+nw)O(w+n-w), that is, O(n)O(n).

In the second case, every time the window moves forward, the new element is smaller than all the other elements in the deque. Therefore, the removeFirst() operation is only performed once in every subsequent step to remove the element that does not fall in the window anymore. So, the time complexity in this case is O(nw+1)O(n - w + 1), that is, O(nw)O(n-w).

In the third case, the same behavior is repeated as in the second case, so the time complexity is O(nw)O(n-w) here, too.

Finally, in the fourth case, the time complexity for increasing values as well as decreasing and constant values will be the same as explained above. The only other situation is when the values increase after staying constant, or right after a sequence of decreasing numbers. In either case, we incur a cost of O(w)O(w), since we clean up the deque using the removeLast() operation. If there is an increase in the value after every ww elements, we pay the O(w)O(w) cost to clean up the deque. This can only happen nw\frac{n}{w} times. The cost of filling up the deque while the elements are constant or decreasing will be O(w)O(w). So, the total cost with such data will be O((w+w)(nw))O((w+w)(\frac{n}{w})), that is, O(2w(nw))O(2w(\frac{n}{w})) or O(n)O(n).

Hence, the time complexity of the solution, considering the worst of these four cases, is O(n)O(n).

Space complexity

The space complexity of this solution is O(w)O(w), where ww is the window size.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.