Skip to main content

Monotonic Deque Implementation and Applications

Complete Java solution for monotonic queue using deque with detailed complexity analysis and practical applications.

Monotonic queues using deque data structures provide optimal solutions for sliding window maximum/minimum problems and maintain sorted order efficiently through smart insertion and removal operations.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. Are we guaranteed that the input array will have at least one element, or should we handle empty inputs?
  2. Do duplicates in the input array affect the solution, and should we treat them as distinct elements?
  3. Should we return indices or values in the result, and is there a specific ordering requirement?
  4. Can we modify the input array or must we work with a copy?
  5. What is the target time complexity for follow-up — O(n) or better?

Restate the pattern or problem type in your own words: This pattern involves using a deque (double-ended queue) to maintain a monotonic sequence — typically either strictly increasing or decreasing — to efficiently solve problems that require tracking the maximum or minimum element in a sliding window, or finding the next greater/smaller element for each array element.

Clarify the signal words and constraint shape that point to it: Key signal words include "sliding window," "maximum/minimum element," "next greater/smaller element," "monotonic stack/queue," and "efficient processing." The constraint shape often involves:

  • Array or stream of elements with ordering constraints
  • Need to process elements in order but maintain some kind of "best" or "optimal" state
  • Time complexity constraints that favor O(n) solutions over O(n²)

Note any assumptions you are making: I'm assuming that the input is a valid array or stream of comparable elements, and that we're not dealing with streaming data where elements arrive continuously (which would require a different approach). I'm also assuming that the problem requires maintaining some kind of optimal element in a moving window or sequence.

Identify whether the pattern is best suited for static, streaming, or online scenarios if relevant: This pattern is primarily suited for static scenarios where all input elements are available upfront. For streaming data, we'd likely want to use a different pattern like a sliding window with a priority queue or maintain state in a more dynamic way.

The pattern works best when we need to efficiently query for optimal elements over windows or sequences while maintaining some form of monotonic property, which makes it particularly useful for problems like "Next Greater Element" or "Sliding Window Maximum."

2. When to Use This Pattern

Signal words and constraint shapes in a problem statement that indicate this pattern:

  • "Sliding window" or "maximum/minimum in window"
  • "Find the next greater/smaller element"
  • "Maintain a sorted or monotonic property over a moving dataset"
  • Problems asking for "best" or "maximum" in a constraint window
  • "Online" or "streaming" data where elements arrive sequentially

Why this pattern fits better than the most common alternatives: This pattern excels when you need to maintain a monotonic property (like decreasing or increasing order) over a sliding window of elements. Compared to alternatives like sorting the window each time (O(n log n)) or using a heap (O(log n) per operation), the deque-based approach provides O(1) amortized access to the extremal elements, making it ideal for problems where you're repeatedly querying the maximum or minimum in a moving window.

Conditions where a variation or related pattern applies instead:

  • If the problem only requires finding maximum/minimum once, a simple linear scan is sufficient
  • For problems with fixed-size windows where order doesn't matter, a simple array or list might suffice
  • When elements can be processed in batches rather than one-by-one, a stack-based approach might work better

The core insight that makes this pattern work: The key insight is that for a sliding window maximum problem, we only care about elements that could potentially be the maximum in future windows. Elements that are smaller than already-seen larger elements will never become maximums, so we can safely remove them from consideration. This allows us to maintain a deque where elements are in decreasing order, ensuring O(1) access to the current maximum.

The monotonic property ensures that we never need to reprocess elements we've already seen, and the deque efficiently maintains only those elements that matter for future window queries.

3. Approach Evolution

Interview communication script:

My first instinct is to solve this with a brute force approach where I check every possible subarray and find the maximum in each, but that's O(n²) because for each starting position I have to scan forward to find the maximum. Let me think about how to optimize this...

I notice that if I maintain a monotonic decreasing queue, I can reduce it to O(n) by avoiding redundant computations. The key insight is that when I process a new element, I can remove elements from the back of my queue that are smaller than the current element, since they can never be the maximum in any future window. This way, I maintain a queue where elements are in decreasing order.

The invariant I need to maintain is that the deque always contains indices of array elements in decreasing order of their values, and I only keep elements that could potentially be the maximum in some future window. When I slide the window, I remove elements that are outside the current window bounds.

Let me trace through this more carefully. The brute force approach would involve:

  1. For each window position, scan all elements in that window
  2. Find the maximum among those elements
  3. This gives us O(n*k) time where k is window size

But I can do better with a monotonic queue:

  1. Maintain a deque that stores indices in decreasing order of their array values
  2. When processing a new element, remove all elements from back that are smaller than current
  3. Add current index to deque
  4. Remove indices that are outside current window bounds from front

This gives me O(n) time because each element is added and removed at most once.

Brute-force approach and why it is not ideal: The naive approach checks every subarray of size k and finds its maximum, resulting in O(n×k) time complexity. This is inefficient because we're recomputing maximums for overlapping windows unnecessarily.

Improved approach and the key optimization: The key optimization is maintaining a monotonic decreasing queue where we only keep elements that could potentially be maximums in future windows. When a new element comes in, we remove all elements from the back that are smaller than current, since they can never be maximums. This ensures the front of queue always contains the maximum for current window.

Final approach with step-by-step reasoning:

  1. Use a deque to store indices of array elements in decreasing order of their values
  2. For each new element, remove smaller elements from back of deque before adding current index
  3. Remove indices that are outside current window bounds from front of deque
  4. The front element represents the maximum for current window
  5. Continue until all elements are processed

Important invariants that must hold:

  • Deque always maintains indices in decreasing order of their array values
  • All elements in deque are within current window bounds or can potentially be in future windows
  • Front of deque always contains index of maximum element in current window
  • Elements are never added to deque more than once, ensuring O(n) time complexity

4. Data Structure Choice

For the monotonic queue pattern, the primary data structure is a Deque (double-ended queue) that maintains elements in monotonic order.

Primary Data Structures Used

  1. ArrayDeque - The main container for maintaining the monotonic queue
  2. Integer array - Input data storage (not part of the algorithm but needed for processing)

Why Each Data Structure Is Needed

The ArrayDeque is chosen because:

  • It supports O(1) operations at both ends (add/remove from front and back)
  • It's optimized for frequent insertions/deletions at both ends
  • It naturally supports the monotonic property maintenance where elements may be removed from either end

The Integer array is needed to store the input elements that will be processed through the monotonic queue.

Time and Space Impact

  • Time Complexity: O(n) for the overall algorithm where n is the input size
    • Each element is added and removed at most once from the deque
  • Space Complexity: O(k) where k is the maximum window size or number of elements maintained in the monotonic queue
    • The deque stores at most k elements at any time

Helper Structure for Simplification

A custom wrapper class or index tracking is helpful to maintain element indices alongside values, which is essential for window-based problems like sliding window maximum.

The deque maintains a monotonic decreasing order where:

  • Elements are stored in decreasing value order from front to back
  • When a new element is added, smaller elements at the back are removed to maintain the invariant
  • This ensures that the front always contains the maximum element in the current window

This structure is optimal for maintaining the monotonic property with minimal overhead while supporting efficient operations at both ends.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to demonstrate how it works.

Example: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3

Step-by-step execution:

  1. Initialization:

    • deque = empty
    • result = []
    • Window size = 3
  2. Processing index 0 (value = 1):

    • deque is empty, so add index 0
    • deque = [0]
    • Window not yet complete (size 1 < 3), no result added
  3. Processing index 1 (value = 3):

    • deque = [0]
    • Value at index 0 (1) < value at index 1 (3), so remove index 0 from deque
    • deque = [1]
    • Add index 1 to deque
    • deque = [1]
    • Window not yet complete (size 2 < 3), no result added
  4. Processing index 2 (value = -1):

    • deque = [1]
    • Value at index 1 (3) > value at index 2 (-1), so keep index 1 in deque
    • Add index 2 to deque
    • deque = [1, 2]
    • Window now complete (size 3), so add max of current window to result
    • Max is at index 1, value = 3
    • result = [3]
  5. Processing index 3 (value = -3):

    • deque = [1, 2]
    • Index 1 (value 3) is out of window (index 1 < current index 3 - 3 + 1 = 1), so remove it
    • deque = [2]
    • Value at index 2 (-1) > value at index 3 (-3), so keep index 2 in deque
    • deque = [2, 3]
    • Window complete (size 3), so add max of current window to result
    • Max is at index 2, value = -1
    • result = [3, -1]
  6. Processing index 4 (value = 5):

    • deque = [2, 3]
    • Index 2 (value -1) is out of window? No, index 2 < current index 4 - 3 + 1 = 2, so keep it
    • Value at index 3 (-3) < value at index 4 (5), so remove index 3 from deque
    • deque = [2]
    • Value at index 2 (-1) < value at index 4 (5), so remove index 2 from deque
    • deque = []
    • Add index 4 to deque
    • deque = [4]
    • Window complete (size 3), so add max of current window to result
    • Max is at index 4, value = 5
    • result = [3, -1, 5]
  7. Processing index 5 (value = 3):

    • deque = [4]
    • Index 4 (value 5) is out of window? No, index 4 < current index 5 - 3 + 1 = 3, so keep it
    • Value at index 4 (5) > value at index 5 (3), so keep index 4 in deque
    • deque = [4, 5]
    • Window complete (size 3), so add max of current window to result
    • Max is at index 4, value = 5
    • result = [3, -1, 5, 5]
  8. Processing index 6 (value = 6):

    • deque = [4, 5]
    • Index 4 (value 5) is out of window? No, index 4 < current index 6 - 3 + 1 = 4, so keep it
    • Value at index 5 (3) < value at index 6 (6), so remove index 5 from deque
    • deque = [4]
    • Value at index 4 (5) < value at index 6 (6), so remove index 4 from deque
    • deque = []
    • Add index 6 to deque
    • deque = [6]
    • Window complete (size 3), so add max of current window to result
    • Max is at index 6, value = 6
    • result = [3, -1, 5, 5, 6]
  9. Processing index 7 (value = 7):

    • deque = [6]
    • Index 6 (value 6) is out of window? No, index 6 < current index 7 - 3 + 1 = 5, so keep it
    • Value at index 6 (6) < value at index 7 (7), so remove index 6 from deque
    • deque = []
    • Add index 7 to deque
    • deque = [7]
    • Window complete (size 3), so add max of current window to result
    • Max is at index 7, value = 7
    • result = [3, -1, 5, 5, 6, 7]

Final result: [3, -1, 5, 5, 6, 7]

Key invariants maintained throughout the algorithm:

  1. deque always stores indices of elements in decreasing order (monotonic property)
  2. The front of deque always represents the index of maximum element in current window
  3. Elements outside current window are removed from deque to maintain valid window size

The algorithm correctly handles all edge cases including negative numbers, and maintains optimal time complexity by avoiding redundant comparisons.

6. Java Solution

import java.util.*;

public class MonotonicQueue {
    
    /**
     * Returns the maximum values in each sliding window of size k
     * Uses a deque to maintain indices of array elements in decreasing order
     * 
     * @param nums Array of integers
     * @param k Size of sliding window
     * @return Array containing maximum values for each window
     */
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0) {
            return new int[0];
        }
        
        // Deque to store indices of array elements
        Deque<Integer> deque = new LinkedList<>();
        int[] result = new int[nums.length - k + 1];
        int resultIndex = 0;
        
        // Process first k elements
        for (int i = 0; i < k; i++) {
            // Remove indices of smaller elements from the back
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }
            deque.offerLast(i);
        }
        
        // Store max of first window
        result[resultIndex++] = nums[deque.peekFirst()];
        
        // Process remaining elements
        for (int i = k; i < nums.length; i++) {
            // Remove indices that are out of current window
            while (!deque.isEmpty() && deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }
            
            // Remove indices of smaller elements from the back
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }
            
            // Add current element index
            deque.offerLast(i);
            
            // Store max of current window
            result[resultIndex++] = nums[deque.peekFirst()];
        }
        
        return result;
    }
}

This solution maintains a deque where indices are stored in decreasing order of their corresponding array values. The front of the deque always contains the index of the maximum element in the current window, enabling O(1) access to the maximum value for each sliding window.

7. Complexity Analysis

Time Complexity: O(n)

The algorithm processes each element in the input array exactly once. Although there's a while loop inside the main loop, each element is added to the deque at most once and removed at most once. This gives us an amortized O(1) time per element, resulting in overall O(n) time complexity.

Space Complexity: O(k)

Where k is the size of the sliding window. In the worst case, the deque will store at most k elements (when all elements in the window are in increasing order). The space complexity is linear with respect to the window size, not the input size.

Why these bounds are correct:

  • Each element is processed exactly once in the outer loop
  • The inner while loop performs at most one removal per element, making it O(1) amortized
  • The deque size is bounded by the window size k, which is typically much smaller than n
  • The algorithm maintains a monotonic decreasing queue, ensuring efficient operations

Optimality:

This solution is optimal for the sliding window maximum problem. Since we must examine each element at least once to determine if it's a maximum in any window, O(n) time complexity is the theoretical lower bound. The deque-based approach achieves this optimal bound while maintaining clean, readable implementation.

The solution is near-optimal in space as well, since we must store at least the window elements to compute the result.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty array: Handle gracefully with early return or proper boundary condition checking
  • Single element: Should work correctly without any queue operations
  • Two elements: Need to ensure proper handling of the monotonic property with just two items

Duplicate Values and Repeated States

  • Duplicate elements: When elements have equal values, the deque must properly maintain the monotonic property (e.g., in a decreasing queue, equal elements may be kept)
  • All identical elements: The algorithm should handle arrays where all elements are the same value
  • Repeated patterns: When the same relative ordering appears multiple times, ensure queue operations don't get into inconsistent states

Boundary Conditions

  • Array with all increasing elements: Should maintain a strictly increasing queue (or decreasing for max sliding window)
  • Array with all decreasing elements: Should maintain a strictly decreasing queue
  • Window size equals array length: The entire array becomes one window, so the result should contain just one element

Common Implementation Mistakes

  • Incorrect deque operations: Adding elements to the wrong end or removing from the wrong end of the deque
  • Not maintaining monotonic property: Failing to remove elements from the deque when new elements break the monotonicity
  • Off-by-one errors: Incorrectly handling window boundaries or element indices when sliding the window
  • Not clearing stale elements: Failing to remove elements that are outside the current window from the deque
  • Wrong comparison operators: Using <= instead of < (or vice versa) when maintaining monotonicity, which can cause issues with duplicate elements

Implementation Specifics

  • Index vs value confusion: Mixing up whether to compare element values or indices when maintaining the queue
  • Queue direction assumption: Assuming the queue is always increasing or decreasing without checking the specific problem requirements
  • Memory leaks in large inputs: Not properly cleaning up the deque when processing very large arrays with many windows

9. Alternative Approaches

There are several alternative approaches to solving problems that might seem to benefit from monotonic deque usage, each with different trade-offs:

1. Sliding Window Maximum with Max Heap Instead of maintaining a monotonic deque, we could use a max heap (PriorityQueue) to track the maximum in each window. However, this approach has O(n log n) time complexity due to heap operations and is more complex to implement correctly with window tracking.

2. Brute Force with Window Traversal For each sliding window, we could iterate through all elements in that window to find the maximum. This is O(n*k) where k is the window size, which is inefficient for large windows or arrays.

3. Two-Pointer Technique with Manual Tracking We could maintain two pointers and manually track the maximum in each window, but this becomes complex to manage and doesn't scale well for larger windows.

4. Segment Tree Approach Using a segment tree to find range maximums would provide O(log n) query time but requires O(n) space and is overkill for simple sliding window problems where we only need to process elements sequentially.

The monotonic deque approach is preferred because it provides O(n) time complexity with clean, intuitive logic that naturally handles the sliding window constraint while maintaining a simple data structure.

The deque-based solution is optimal for this specific pattern because it maintains the necessary invariants (monotonicity) while allowing efficient insertion, deletion, and access at both ends. The key insight is that we only need to keep elements that could potentially be the maximum in future windows, making the deque size bounded by the window size.

10. Test Cases

public class TestCases {
    public static void main(String[] args) {
        // Test Case 1: Basic case with increasing values
        int[] nums1 = {1, 3, -1, -3, 5, 3, 6, 7};
        int k1 = 3;
        // Expected: [3, 3, 5, 5, 6, 7]
        
        // Test Case 2: Decreasing values
        int[] nums2 = {7, 7, 7, 7, 7};
        int k2 = 3;
        // Expected: [7, 7, 7, 7]
        
        // Test Case 3: Empty array
        int[] nums3 = {};
        int k3 = 1;
        // Expected: []
        
        // Test Case 4: Single element
        int[] nums4 = {5};
        int k4 = 1;
        // Expected: [5]
        
        // Test Case 5: Window size equals array size
        int[] nums5 = {1, 2, 3, 4, 5};
        int k5 = 5;
        // Expected: [5]
        
        // Test Case 6: Negative numbers
        int[] nums6 = {-1, -2, -3, -4, -5};
        int k6 = 2;
        // Expected: [-1, -2, -3, -4]
        
        // Test Case 7: Mixed positive and negative
        int[] nums7 = {1, -1, 2, -2, 3, -3};
        int k7 = 3;
        // Expected: [2, 2, 3, 3]
    }
}

Sample Test Cases:

  1. Basic case: [1,3,-1,-3,5,3,6,7] with k=3 → [3,3,5,5,6,7]
  2. All same elements: [7,7,7,7,7] with k=3 → [7,7,7,7]
  3. Empty input: [] with k=1 → []
  4. Single element: [5] with k=1 → [5]
  5. Window equals array size: [1,2,3,4,5] with k=5 → [5]
  6. All negative: [-1,-2,-3,-4,-5] with k=2 → [-1,-2,-3,-4]
  7. Mixed signs: [1,-1,2,-2,3,-3] with k=3 → [2,2,3,3]

The test cases cover various edge conditions including empty arrays, single elements, uniform values, negative numbers, and boundary conditions where window size equals array size. These test cases ensure the monotonic deque solution handles all typical scenarios correctly.

11. Final Summary

The core idea is to maintain a monotonic deque that efficiently tracks the maximum element in sliding windows by removing smaller elements from the back and outdated elements from the front. This solution works because it preserves the invariant that the front of the deque always contains the maximum element in the current window, enabling O(n) time complexity by ensuring each element is added and removed at most once. The key efficiency comes from avoiding redundant comparisons through the monotonic property, making it optimal for this sliding window maximum problem.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Patterns

    Sliding Window Maximum Problem Solution

    Complete Java implementation of the sliding window maximum problem using deque data structure with O(n) time and space complexity analysis.

  • DSA Problems

    Sliding Window Maximum Problem Solution

    Complete Java implementation of the sliding window maximum problem using deque data structure with O(n) time and space complexity analysis.

  • DSA Problems

    Valid Parentheses Algorithm Solution

    Complete Java implementation for validating balanced parentheses using stack data structure with O(n) time and space complexity.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.