Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 24

Mastering Deques: Sliding Window & Beyond

Recommended resources

Hello! Welcome back to our series on high-ROI interview patterns.

In our last lesson, we explored the monotonic stack, a clever tool for finding the "next greater/smaller element" in linear time. We saw how maintaining a sorted order within the stack allowed us to avoid inefficient nested loops. Today, we'll adapt that powerful concept to solve a different but related problem: finding the maximum in a sliding window.

Your goal for this lesson is to learn how to implement and apply a deque (double-ended queue) to solve problems like Sliding Window Maximum. This is a classic, hard-tagged interview problem (like LeetCode 239) that elegantly demonstrates the power of using the right data structure. Mastering this pattern is a strong signal to interviewers that you can devise efficient, non-obvious solutions.

1. Understanding the Problem and the Tool

The problem is "Sliding Window Maximum." You are given an array and a window size k. As this window slides from left to right, you need to report the maximum value within the window at each position.

Sliding Window Maximum Visualization
This image shows a window of size k=3 sliding across an array. For each position, the maximum element inside the window is found, and these maximums form the output array.

The brute-force approach is to iterate through each of the n-k+1 windows and then loop through the k elements inside to find the maximum. This gives a time complexity of O(n * k), which is too slow for typical interview constraints. We need a linear time, O(n), solution.

To achieve this, we introduce the deque (pronounced "deck"), or double-ended queue. As its name implies, it's a sequence container that allows for efficient insertions and deletions at both its front and back ends. In Python, this is readily available as collections.deque. Operations like append, pop, appendleft, and popleft are all O(1).


2. The Monotonic Deque Strategy

The key to an O(n) solution is to avoid re-scanning the window. How can we find the maximum in O(1) time for each window? This is where the connection to our previous lesson comes in. We will maintain a monotonic deque.

Specifically, we'll use the deque to store the indices of elements from the current window. We will enforce a rule: the values corresponding to the indices in the deque must be in decreasing order from front to back.

Why does this work?

  • The Maximum is Always at the Front: Because the deque is sorted by value in decreasing order, the index at the very front (dq[0]) will always correspond to the largest element in our list of candidates.
  • Efficiently Discarding Useless Elements: If a new, larger element nums[i] arrives, any smaller elements currently in the deque that come before it are now useless. They can never be the maximum in any future window that includes nums[i]. We can therefore pop them from the back.

Let's watch a video that explains this core idea and contrasts it with the brute-force method.

Sliding Window Maximum - Monotonic Queue - Leetcode 239

The video 'Sliding Window Maximum' by NeetCode provides an excellent explanation of the problem and the intuition behind using a monotonic deque for an optimal solution.

Please watch from the beginning until 05:31. Focus on two key points: why the brute-force O(n*k) approach is inefficient, and the logic for using a decreasingly ordered deque to eliminate elements that can no longer be the maximum.


3. The Algorithm in Detail

Let's formalize the step-by-step logic for building our solution.

  1. Initialize an empty deque dq (which will store indices) and an empty list result.
  2. Iterate through the input array nums from left to right with index i. For each element nums[i]:
    • Clean up the front: Check if the index at the front of the deque is now out of the current window's bounds (i.e., dq[0] <= i - k). If it is, popleft() to remove it.
    • Clean up the back: While the deque is not empty and the current element nums[i] is greater than the element at the index at the back of the deque (nums[dq[-1]]), pop() from the back. This removes all smaller elements that are now irrelevant.
    • Add the new element: append() the current index i to the deque.
    • Record the maximum: If the window is fully formed (i.e., i >= k - 1), the maximum for this window is the element at the deque's front index. Append nums[dq[0]] to the result list.
  3. Return the result list.

To solidify this process, let's follow a detailed dry run. The following article provides a fantastic visual walkthrough.

Sliding Window Maximum - Tutorial

The article 'Sliding Window Maximum - Tutorial' from takeuforward.org has a great set of images illustrating the state of the deque as the window slides. It reinforces the algorithm we just discussed.

Read the 'Optimal Approach' section. Pay close attention to the sequence of six images that show the deque's state for the array [4,0,-1,3,5,3,6,8] with k=3. This will help you visualize the algorithm's three main steps: cleaning the front, cleaning the back, and adding the result.

Test your understanding!

Let's trace the algorithm for a simple case.
Input: nums = [8, 7, 6, 9], k = 2

What will be the state of dq (storing indices) and result after each iteration i?

Show answer

Initial state: dq = deque([]), result = []

  1. i = 0, nums[0] = 8:

    • Front cleanup: (skip, dq empty)
    • Back cleanup: (skip, dq empty)
    • Add index: dq = deque([0])
    • Record result: (skip, i < k-1)
  2. i = 1, nums[1] = 7:

    • Front cleanup: (skip, dq[0] is not _ <= 1-2)
    • Back cleanup: 7 > nums[dq[-1]] (7 > 8) is false.
    • Add index: dq = deque([0, 1])
    • Record result: i >= k-1 is true. result.append(nums[dq[0]]) -> result.append(8).
    • State: dq = deque([0, 1]), result = [8]
  3. i = 2, nums[2] = 6:

    • Front cleanup: dq[0] <= i - k (0 <= 2 - 2) is true. dq.popleft(). dq is now deque([1]).
    • Back cleanup: 6 > nums[dq[-1]] (6 > 7) is false.
    • Add index: dq = deque([1, 2])
    • Record result: i >= k-1 is true. result.append(nums[dq[0]]) -> result.append(7).
    • State: dq = deque([1, 2]), result = [8, 7]
  4. i = 3, nums[3] = 9:

    • Front cleanup: dq[0] <= i - k (1 <= 3 - 2) is true. dq.popleft(). dq is now deque([2]).
    • Back cleanup: 9 > nums[dq[-1]] (9 > 6) is true. dq.pop(). dq is now deque([]).
    • Add index: dq = deque([3])
    • Record result: i >= k-1 is true. result.append(nums[dq[0]]) -> result.append(9).
    • State: dq = deque([3]), result = [8, 7, 9]

Final Result: [8, 7, 9]


4. Implementation and Complexity

Now, let's look at the code. Since you are proficient in Python and it's a common language for interviews, we will focus on that implementation.

Sliding Window Maximum - Monotonic Queue - Leetcode 239

Let's return to the NeetCode video to see the full Python implementation and a final summary of the logic.

Watch from 05:31 to the end. The first part (until 12:02) is a detailed walkthrough of another example. The second part (from 12:02) presents and explains the Python code that implements our algorithm.

Here is the Python code from the takeuforward article for your reference. Notice how closely it maps to the algorithm steps we defined.

from collections import deque
from typing import List

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq = deque()
        result = []

        for i in range(len(nums)):
            # 1. Remove indices that are out of the current window's scope
            if dq and dq[0] <= i - k:
                dq.popleft()

            # 2. Remove smaller elements from the back of the deque
            while dq and nums[dq[-1]] < nums[i]:
                dq.pop()

            # 3. Add the current element's index to the deque
            dq.append(i)

            # 4. If the window is fully formed, append the max (at the front) to the result
            if i >= k - 1:
                result.append(nums[dq[0]])

        return result

Complexity Analysis

  • Time Complexity: O(n)
    This is the most important takeaway. Although there is a while loop inside the for loop, the analysis is amortized. Each element's index is pushed onto the deque exactly once and popped from the deque at most once throughout the entire process. Therefore, the total work is proportional to the number of elements, n, giving us a linear time complexity.
  • Space Complexity: O(k)
    The deque stores indices. In the worst-case scenario (a strictly decreasing array), the deque will hold up to k indices at one time, one for each element in the window. The result array does not count towards auxiliary space complexity in most interview contexts.

You can review a concise summary of the complexity in the final section of the takeuforward article (resource ID LINK).


Conclusion

Congratulations on mastering another advanced, high-ROI interview pattern! You have seen how to move from a naive O(n*k) solution to an optimal O(n) solution for the Sliding Window Maximum problem by using a deque to maintain a monotonic collection of candidates. This demonstrates a deep understanding of how data structures can be creatively applied to solve complex problems efficiently.

Key Takeaways:

  • A deque provides O(1) insertion and deletion at both ends, making it perfect for managing a sliding window's contents.
  • By maintaining a deque of indices whose corresponding values are in decreasing order, we ensure the maximum of the current window is always at the front, ready for O(1) access.
  • The overall time complexity is O(n) because each element is processed (pushed and popped) a constant number of times over the entire traversal.
  • This pattern is a powerful combination of the sliding window technique and the monotonic principle we learned in the last lesson.

We've now covered several key patterns for linear data structures. In our next lesson, we'll shift our focus to non-linear structures and begin our exploration of Binary Trees, starting with the fundamental traversal algorithms: pre-order, in-order, and post-order.