Tutorial on LeetCode 239. Sliding Window Maximum (Hard)

Grant Ferowich
8 min readJul 30, 2023

--

Curious Weekly — writing inspired by curiosity.

This post is for people solving sliding window problems during their LeetCode journeys.

  1. Recognizing sliding window problems
  2. Methodological approach to solving sliding window problems
  3. The approach for 239. “Sliding Window Maximum”
  4. Time and space complexities
  5. Edge case
  6. A note on the type of linear data structure in the input: array or string
  7. Softly typed JavaScript

Recognizing sliding window problems

Sliding window problems (SWPs) are a relatively common problem category you will see during interviews. The main idea with a sliding window problem is that you are given a linear data structure, such as a string or an array, and you must optimize for some constraint around a minimum or maximum number of things which must be true about a collection of items within the input string or input array. So, for example, some classic sliding window problems are “Longest Substring Without Repeating Characters,” “Longest Repeating Character Replacement,” “Minimum Window Substring,” and “Sliding Window Maximum.” The general idea for all sliding window problems is that you maintain a window while traversing across the linear data structure. The window is some sub-collection of elements inside the collection of elements contained in the array or string passed as a parameter.

We recognize the problem as a sliding window problem because 1, the question is literally called “Sliding Window Maximum,” and 2, there is some invariant to be maintained while keeping track of some piece of data the question asks us to return. Recognizing the question as a sliding window problem and not a graph, priority queue or linked list problem can only aid in fully understanding the question.

Methodological approach to solving sliding window problems

There are basic questions you want to answer in simple terms before implementing and writing your code. These questions are in play for every SWP.

  1. Does the question ask me to implement a fixed-sized sliding window or a dynamically-sized sliding window?
  2. Under what condition does the right pointer move?
  3. When will the function read the temporary solution?
  4. How will the function decide on the optimal solution?
  5. Under what condition does the left pointer move?
  6. Which category does the SWP fall under?

Next we can think about the form of the sliding window problem. Because sliding window problems always solve for some maximum or minimum subset of the input array or input string, there are only so many ways a sliding window problem can be presented.

  1. Fast/slow: The right pointer moves faster than the left pointer. The question covered by this post on LeetCode 239: “Maximum Window Substring” is a fast/slow SWP.
  2. Fast/catchup: The only difference between the fast/slow problems and the fast/catchup problems is that fast/catchup problems have a left pointer which “jumps” to the location of the right pointer. LeetCode 53. “Max Subarray” is an example. The jump occurs conditionally upon some event happening such as the currentSumInteger in “Max Subarray” becoming negative.
  3. Fast/lagging: While looping over the array the function maintains an invariant where the solution can only be changed by an element which is a fixed number of spots away from the present array value. See the “House Robber” problem.
  4. Front/back: The left pointer is initialized at the 0th index while the other pointer is initialized at the last index. See “Trapping Rain Water.”

Since every sliding window problem must fall into one of these four categories, we can ask “which category does the present SWP fall under?” We know the answer will always be one of these four choices: Fast/slow, fast/catchup, fast/lagging, and front/back.

The approach for “Sliding Window Maximum”

  1. Does the question ask me to slide a fixed-sized window or a dynamically-sized window?
  • A fixed-sized window with a length of k.

2. Under what condition does the right pointer move?

  • The right pointer moves as long as the index of the right pointer is less than the length of the input array.

3. When will the function read the temporary solution?

  • The function reads the temporary solution after each time the right pointer’s index changes. The temporary solution in “Sliding Window Maximum” to keep track of is the maximum value which has been observed.

4. How will the function decide on the optimal solution?

  • As long as the right pointer index is less than the length of the input array, and as long as the size of the window equals k, then whatever the current maximum value which has been observed so far is pushed to the output array.

5. Under what condition does the left pointer move?

  • The left pointer moves when the window size has reached k.

6. Which category does the present SWP fall under?

  • “Sliding Window Maximum” is a fast/slow SWP.
const slidingWindowMaximum = (numsArr, kInt) => {
if (kInt > numsArr.length){
return [];
}
let rightPtrInt = 0;
let leftPtrInt = 0;
let maxValInt = -Infinity;
let maxValIndexInt = -1;
let outputArr = [];
while (rightPtrInt < numsArr.length){
let numInt = numsArr[rightPtrInt];
if (numInt >= maxValInt){
maxValInt = numInt;
maxValIndexInt = rightPtrInt;
}
rightPtrInt++;
if (rightPtrInt - leftPtrInt === kInt){
outputArr.push(maxValInt);
// handle the case where the maxVal leaves when leftPtrInt is about to moves
if (maxValIndexInt === leftPtrInt){
maxValInt = -Infinity;
// perform O(kInt) scan to find and store the maximum value in the window
for (let xInt = leftPtrInt+1; xInt < rightPtrInt; xInt++){
let numInt = numsArr[xInt];
if (numInt >= maxValInt){
maxValInt = numInt;
maxValIndexInt = xInt;
}
}
}
leftPtrInt++;
}
}
return outputArr;
}

/* Tests */
let nums1Arr = [1,3,-1,-3,5,3,6,7];
let k1Int = 3;

let nums2Arr = [1];
let k2Int = 1;

let nums3Arr = [1, -1];
let k3Int = 1;

let nums4Arr = [1,3,1,2,0,5];
let k4Int = 3;


console.log(slidingWindowMaximum(nums1Arr, k1Int)); // Expect [3,3,5,5,6,7]
console.log(slidingWindowMaximum(nums2Arr, k2Int)); // Expect [1]
console.log(slidingWindowMaximum(nums3Arr, k3Int)); // Expect [1, -1]
console.log(slidingWindowMaximum(nums4Arr, k4Int)); // Expect [3,3,2,5]

Time & space complexities

The method I share here has a run-time of O(k*(N-k)). The window is size k. The function must scan the window N-k times in order to find the maximum value of the array.

“Wait,” you say, “I thought a sliding window problem can be implemented efficiently with a time complexity of O(N).” You would be right. There is a reason for implementing the O(k*(N-k)) first. This LeetCode problem is technically classified as a “hard” problem. While the solution presented here is not the most efficient solution, writing out this solution will help us to develop the intuition for solving any sliding window problem in the future. I chose to write about this problem because there is a very simple and straightforward notion of what a sliding window is. Like other sliding window problems, part of the problem consists of building the window, and part of the problem consists of doing something with the elements within the window, and then there is the straightforward notion of sliding the window over the input. Also, for what it’s worth, with the LeetCode tests as they are written the O(k*(N — k)) solution will pass. Do you have suggestions about how to implement a more efficient function in terms of time complexity? Leave your thoughts in the comment section below.

The space complexity of the function is O(N-k+1). The output array uses O(N) space, but since the window size is k in length, the space complexity is slightly less than N. The space complexity is actually just O(N-k+1). The number of elements in the output array is equal to how many times the window can be slid over the input array, or O(N-k) plus the maximum value of the window before the window begins to slide, which is O(1).

Edge case

The problem “Sliding Window Maximum” asks us to keep track of the maximum value inside a sliding window and cache the max value as the window slides over the input array. How big is the window sliding over the input array? The window’s size is fixed and not dynamic. Specifically, there are exactly k elements inside the window, where k is just an integer passed as a parameter. We also know if k is greater than the length of the input array, there is no possible solution. Sliding a window with a size k=5 elements over an array containing 3 elements is impossible.

// Example 1
kInteger1 = 5;
array1 = [ 1, 2, 3];
// Diagram of the window before the window has moved: [ 1, 2, 3, _, _ ]
// Result: [], an empty array

As we can see, when kInteger1 is greater than the length of array1 there is no valid window which can slide over the input array.

A note on the type of linear data structure in the input: array or string

I write functions in JavaScript, where you can convert strings into arrays using the .split() method. For example, you can convert str1 = “Chicago” into arr1 = [ “C”, “h”, “i”, “c”, “a”, “g”, “o”] by assignment with let arr1 = str1.split(“”). You can also convert an array into a string using the .join() method. Returning arr1.join(“”) returns “Chicago.” The point is this: don’t let the specific linear data structure worry you too much. If you really want to convert a string to an array because you prefer the methods associated with an array, that option is completely available for you. You actually might be really interested in using an array instead of a string since arrays have more methods available in JavaScript. Arrays also have methods which enable the array to be implemented as a queue, stack, or double-ended queue (deque).

You generally will be hard-pressed to find a moving window solution faster than O(N) time, also known as linear time, when analyzing the time complexity of a successful sliding window function. Since the optimal solution’s runtime is not more efficient than O(N), applying .split(“”) or .join(“”) is not going to adversely affect the overall runtime of your solution.

Softly typed JavaScript

If you have written in JavaScript as well as another language, such as C# or Java, you are surely aware a variable can be declared in JS without specifying the variable’s data type. There’s a lot to like about specifying what the data type of a particular variable will be. I have been writing JavaScript variables with this formula: variableName + Int/Map/Set/Arr/Str/Node. Specifying the variable type at the end of the variable’s name has proved clarifying and helpful when referencing a variable later. The variable’s name clearly tells you as a developer what data type to expect. No more re-checking your code to see whether a variable is an integer, a string, or something else. Let me know your thoughts on softly typed JavaScript in the comments.

That’s it for this post! I hope you have an amazing day and a wonderful journey as an engineer.

Curious Weekly — writing inspired by curiosity.

--

--

Grant Ferowich

Writing code. "Everyone you meet is fighting a battle you know nothing about. Be kind." Curious Weekly - https://curiousweekly.com/.