DSA: Monotonic Queue Pattern
Solution to Last Week's Challenge
Last week's problem was: DSA: Greedy Algorithms Patterns
The optimal solution uses dsa: greedy algorithms patterns pattern with careful state management.
Complexity Analysis:
- Time Complexity: O(n)
- Space Complexity: O(1)
In the landscape of technical interviews, few patterns separate senior engineers from juniors as effectively as the Monotonic Queue. While most candidates are comfortable with standard Queues and Stacks, the Monotonic Queue is a specialized tool designed to solve range-query problems in linear time. It is the "secret weapon" for optimizing problems that initially seem to require nested loops or complex segment trees. In real-world systems, this pattern is foundational for low-latency stream processing, sliding window analytics in financial tickers, and resource scheduling where we need to maintain the extremum (minimum or maximum) of a moving data window.
Mastering this pattern is less about the data structure itself—which is typically implemented using a Double-Ended Queue (Deque)—and more about the logic of "maintenance." The core idea is to maintain a queue where elements are always sorted in a specific order (increasing or decreasing). By strategically removing elements that can no longer be the "best" candidate for future windows, we achieve an amortized time complexity of O(1) per operation, transforming O(N*K) or O(N log N) problems into elegant O(N) solutions.
Theory
A Monotonic Queue is not a standard library structure in most languages but a logical wrapper around a Deque. Its primary characteristic is that it preserves a strict order of elements. In a Monotonic Decreasing Queue, the elements are ordered from largest to smallest. When a new element arrives, we remove all elements from the back of the queue that are smaller than the new arrival before inserting it. This ensures the maximum element of the current window is always at the front.
The complexity analysis is often counter-intuitive to beginners. Although we use a while loop inside a for loop during the push operation, each element is added to the Deque exactly once and removed exactly once. This results in a total of 2N operations, making the amortized time complexity O(1) per element.
Pattern Visualization
The logic follows a strict "discard the obsolete" policy. An element becomes obsolete if it is outside the current sliding window or if a newer, "better" element (e.g., a larger value in a max-queue) has entered the sequence.
Implementation
Let’s look at the classic "Sliding Window Maximum" problem. Given an array and a window size k, we need to find the maximum value in every sliding window.
Brute Force Approach
The naive solution iterates through every possible window and finds the maximum.
def sliding_window_max_brute(nums, k):
# Time Complexity: O(N * K)
# Space Complexity: O(1) - excluding output
res = []
for i in range(len(nums) - k + 1):
res.append(max(nums[i:i + k]))
return resOptimized Monotonic Queue Approach
By using a Deque to store indices, we maintain a decreasing order of values.
from collections import deque
def sliding_window_max_optimized(nums, k):
# Time Complexity: O(N)
# Space Complexity: O(K)
dq = deque() # Stores indices
res = []
for i in range(len(nums)):
# 1. Remove indices out of the current window
if dq and dq[0] < i - k + 1:
dq.popleft()
# 2. Maintain monotonicity (Decreasing)
# If current value is larger than the back, the back can't be the max
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# 3. Add to result once the first window is filled
if i >= k - 1:
res.append(nums[dq[0]])
return resComplexity Analysis Table
| Approach | Time Complexity | Space Complexity | Best For |
|---|---|---|---|
| Brute Force | O(N * K) | O(1) | Small window sizes, simple logic |
| Segment Tree | O(N log N) | O(N) | Dynamic updates to the array |
| Monotonic Queue | O(N) | O(K) | Static arrays, sliding windows |
| Heaps (Priority Queue) | O(N log K) | O(K) | When window size is massive and N is small |
Common Patterns
The Monotonic Queue is frequently used in two specific variations:
1. Finding the Nearest Smaller/Larger Element in a Window This is common in histogram problems or "constrained subsequence" problems.
# snippet: maintaining a minimum in a sliding window
while dq and nums[dq[-1]] > nums[i]:
dq.pop()
dq.append(i)2. Constrained Dynamic Programming
In DP problems where dp[i] = nums[i] + max(dp[j]) for i-k <= j < i, a monotonic queue optimizes the search for max(dp[j]) from O(K) to O(1).
Practice Problems
To master this pattern, you must recognize when a problem asks for an extremum over a range.
This Week's Interview Challenge
Problem: Shortest Subarray with Sum at Least K
Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1. Note that the array may contain negative numbers, which prevents a standard two-pointer sliding window approach.
Example 1:
Input: nums = [2, -1, 2], k = 3
Output: 3
Example 2:
Input: nums = [1, 2], k = 4
Output: -1
Constraints:
1 <= nums.length <= 10^5-10^5 <= nums[i] <= 10^51 <= k <= 10^9
Hints:
- Use a prefix sum array to represent the sum of elements.
- If
P[j] - P[i] >= k, we are looking for the minimumj - i. - If
P[i]is greater than a subsequent prefix sumP[j],P[i]can never be the start of a "shortest" subarray for any future index, becauseP[j]is smaller and appears later. - Maintain a Monotonic Increasing Queue of prefix sum indices.
Template Code:
from collections import deque
def shortest_subarray(nums, k):
# Your code here
pass
# Submit your solution and check back next week for the detailed answer!Conclusion
The Monotonic Queue is a sophisticated optimization of the sliding window pattern. Its power lies in its ability to prune the search space by removing elements that are "guaranteed to lose." When you encounter a problem involving a sliding window and a requirement for a maximum or minimum, the Monotonic Queue should be your first consideration. In an interview, always start by identifying the "obsolescence criteria"—what makes an element useless for future windows? Once you define that, the implementation follows naturally.