SerialReads

Monotonic Queues — Sliding-Window Power Tool for O(n) Max/Min, DP & Graph Optimisation

Jun 20, 2025

Great — I’ll prepare a ~1,200-word article introducing monotonic queues for intermediate-to-advanced software engineers and SDE interview candidates. It’ll include:

I’ll get started and let you know as soon as it’s ready.

Monotonic Queues — Sliding-Window Power Tool for O(n) Max/Min, DP & Graph Optimisation

Executive Summary

A monotonic queue (usually implemented as a deque) is a specialized data structure that keeps its elements in sorted order (either non-increasing or non-decreasing) at all times. This simple idea yields powerful results: it lets you fetch the maximum or minimum in a sliding window in amortised O(1) time per element (total O(n) for an array scan). Monotonic queues shine whenever you need to efficiently track an extreme (max/min) over a moving window or range – common in streaming data analytics, rolling metrics, and coding interview problems. They also unlock linear-time solutions to certain dynamic programming recurrences (by turning a costly range-max or min query into constant time) and even speed up graph algorithms for weighted shortest paths. In scenarios that involve sequential monotonic queries, reaching for a monotonic queue can eliminate heaps or segment trees and dramatically reduce complexity, as long as queries are limited to “max” or “min” operations. (For more complex or unordered queries like medians or percentiles, other structures are preferable.) In short, use monotonic queues for sliding extremes and DP optimisations to get heap-like power at deque-like speeds.

From FIFO to “FIFO-with-Pruning”: How Monotonic Queues Work

Imagine a normal FIFO queue that, somehow, always keeps the largest element at the front. A monotonic queue achieves this by pruning out weaker elements as new ones arrive. In a decreasing monotonic queue (for tracking maximums), every push ejects any smaller values lurking at the back, since they can never be the max if a bigger element came in after them. Dually, an increasing monotonic queue pops out any larger values on a push (useful for minimums). This way, the queue’s internal order is maintained as always decreasing (for max-queue) or increasing (for min-queue). Crucially, we still add and remove from opposite ends like a normal deque (new elements go in the back; old elements get removed from the front), preserving FIFO order except that some elements get dropped early (“pruned”) by the invariant. The result: at any given time, the front of the deque is the current maximum (or minimum) of all elements in it.

Why does this help? Because if you slide a window across an array, you can maintain its max/min in constant time using this deque. As the window moves forward, you pop from the front when the oldest element leaves the window, and push new elements at the back with the pruning step to drop obsolete smaller values. Each element is added and removed at most once over the whole process, ensuring the total cost stays linear. The deque effectively behaves like a “max queue” or “min queue” supporting three operations: push (with pruning), pop (from front), and peek (front element) – all in amortised O(1) time. This FIFO-with-pruning trick is the heart of the monotonic queue’s power.

Sliding-Window Maximum in O(n) (Step-by-Step)

To cement the idea, consider the classic Sliding Window Maximum problem: Given an array nums and window size k, find the max in each window of length k as it slides along. A naïve solution checks all k elements for each window (O(n·k)), and a heap-based solution improves it to O(n log n), but a monotonic deque nails it in O(n). Here’s how it works step-by-step:

For example, take nums = [1,3,-1,-3,5,3,6,7] with k=3. The monotonic deque process would produce:

This yields outputs [3,3,5,5,6,7] which matches the expected results. Each element was enqueued once and dequeued (or pruned) at most once, for a total of ~2n deque operations, guaranteeing linear time overall. In fact, the monotonic queue method is the fastest possible for this problem — data structures like heaps or segment trees will be slower by a logarithmic factor.

Dynamic Programming Speed-ups (Sliding Window DP Optimization)

Monotonic queues aren’t limited to explicit “sliding window” problems – they also excel in dynamic programming optimizations where a state transitions from a window of previous states. A common scenario is a DP recurrence like:

$ dp[i] = \text{value}[i] + \max_{j \in [i-k,; i-1]} dp[j] ,$

where each state depends on the maximum of the last k DP values (or minimum, in other cases). Computing this directly is O(n·k), but we can treat “max of last k dp[j]” as a sliding-window query and maintain it with a monotonic deque in O(n). This trick turns many seemingly quadratic DP problems into linear ones.

Jump Game VI (LeetCode 1696) is a canonical example. You must jump through an array with at most k steps at a time, maximizing your score. The straightforward DP is: dp[i] = nums[i] + max(dp[i-1...i-k]). Using a deque to hold indices of the best recent dp values (dropping any dp that’s lower than a new candidate) gives a linear solution. Similarly, Constrained Subsequence Sum (LC 1425) asks for the maximum sum subsequence with distance ≤ k between elements – which leads to dp[i] = nums[i] + max(0, dp[i-1...i-k]). A monotonic queue over the dp array achieves O(n) time. In both cases, the deque logic mirrors the sliding window maximum: as i increases, remove dp values that fall out of the [i-k, i-1] range from the front, and remove any smaller dp values from the back before adding dp[i-1] or dp[i] in. This technique can drastically speed up DP solutions that otherwise would timeout, and it’s easier to implement than heavy alternatives like segment trees or heaps (which would give O(n log n) time). Whenever you see a DP that looks at a min/max over a fixed recent range, think monotonic queue optimization.

Graph & Path-Finding: 0–1 BFS and Dial’s Algorithm

Monotonic queues even appear in graph algorithms, notably for shortest paths in graphs with small integer weights. A prime example is the 0–1 BFS algorithm for graphs where each edge weight is either 0 or 1. Normally, Dijkstra’s algorithm would handle this in O((V+E) log V) time with a priority queue. But we can do better: use a deque to store nodes to visit, and when traversing an edge of weight 0, push the new node to the front of the deque (since its distance is the same as the current node), and for weight 1 edges, push the node to the back. This way, the deque always processes nodes in increasing order of distance, effectively acting like a monotonic queue of distances. The result is an O(V+E) algorithm – each edge is relaxed exactly once, and the data structure overhead is just O(1) per edge. In essence, the deque here maintains a monotonic order of distances (non-decreasing from front to back), without needing a heap.

For example, if you have a deque of nodes sorted by their current tentative distance, and you explore a 0-weight edge, the neighbor gets the same distance and is queued at the front (so it’s processed immediately, as it should be). A 1-weight edge gives a neighbor distance +1, queued at back. This simple tweak keeps the distance queue sorted and eliminates the need for heap operations. This is exactly how 0–1 BFS operates, and it’s widely used for problems with binary weights.

Dial’s algorithm generalizes this idea to small positive weights (say 0 to k). Instead of a single deque, it uses an array of k+1 buckets (queues) to hold nodes by distance mod (k+1), effectively maintaining multiple monotonic queues that cycle through distances. The bucket corresponding to the current minimum distance is processed, and when it’s empty, you move to the next bucket. This yields O(V + E) time for weights up to k (often used for shortest paths in graphs with limited-weight edges). Both 0–1 BFS and Dial’s algorithm demonstrate how maintaining ordered queues can replace heavier structures in graph traversal when weight constraints allow.

Complexity: Amortised O(1) Operations vs. Heaps and Trees

Time Complexity: The monotonic queue achieves amortised O(1) time per operation (push, pop, peek), which translates to O(n) overall for processing n elements. The intuition is that each element is pushed once and popped at most once. Any element can only be removed in two ways: either it’s popped from the front when it becomes stale (leaves the window), or it’s pruned from the back by a larger element entering. Each element thus causes at most one push and one pop, i.e. O(2n) ~ O(n) operations total. Compare this with a heap/priority queue approach to sliding window max: every push or pop is O(log n), and you might do ~2n operations (push new, remove old), for O(n log n) in total. A balanced BST or skip list gives similar log-factor overhead. A segment tree or Fenwick tree can answer range max queries in O(log n) as well, leading to O(n log n) for n windows. Thus, the monotonic deque is asymptotically faster than these, and the difference is significant for large n. It’s also simpler to implement and often has lower constant factors (just a few comparisons and pointer moves per element).

Space Complexity: A monotonic queue stores at most one index or element per input element, so in sliding window scenarios it uses O(k) space (in worst case, it could hold all elements if they are strictly monotonic). This is optimal for holding window content. By contrast, a heap might also hold O(k) elements, and a segment tree uses O(n) memory regardless. So memory usage is comparable or better, and often the monotonic approach has better locality (working mostly at ends of a deque).

When do these gains apply? Only for monotone queries (max or min). If you need arbitrary order-statistics (median, kth largest) or combine results (e.g. sum, average), monotonic queues won’t help because they only maintain one extreme. For those, other data structures or algorithms are needed. But when you specifically need fast min or max in a sliding window or range, monotonic queues are the gold standard. They provide an elegant O(n) solution where a general data structure would be O(n log n).

Edge Cases & Pitfalls

Like any technique, monotonic queues have some nuances and edge cases to watch out for:

Overall, the pitfalls are minor – just make sure to index correctly and purge old elements, and the structure will maintain the correct invariant automatically.

Monotonic queues come in a few flavors and have cousins useful in similar scenarios:

Implementation Patterns and Tips

Implementing a monotonic queue is straightforward, and you can encapsulate it for reuse. The core idea is to store indices and/or values and enforce the sorted invariant on each push. Common patterns include:

By encapsulating the logic, you avoid off-by-one errors each time and can reuse it for any window size or even unbounded streaming scenarios (where you manually decide when to pop from front).

Real-World Scenarios

Monotonic queues aren’t just theoretical interview tools; they appear in real systems wherever a rolling max/min needs to be computed efficiently:

In short, whenever you have a continuous stream and you’re interested in extremes over a recent window, monotonic queues are likely the data structure of choice in production systems due to their efficiency.

When Not to Use a Monotonic Queue

While monotonic queues are powerful for what they do, it’s important to recognize when they are not the right tool:

In summary, don’t use a monotonic queue when the query isn’t max/min, or when the window size or performance constraints demand a different approach. But whenever you specifically need sliding maxima or minima, a well-implemented monotonic queue is usually unbeatable in simplicity and speed.

Monotonic Queue Cheat-Sheet 📝

Operation Description Amortised Cost Typical Use-Case
push(x) Insert new element x, popping all smaller elements from the back to maintain sorted order (for max-queue; pop larger for min-queue). O(1) amortised Add next element of sliding window
pop_front() Remove the element from the front if it falls out of the window (i.e. its index is out of range). In a generic queue context, this just dequeues the oldest element. O(1) Slide window, remove old element
peek()/front Query the current extreme (max or min) at the front of the deque. This is always valid in O(1) time because of the maintained invariant (the front is the max/min). O(1) Get current window’s max or min
Size/Space At most k elements in deque for window size k (worst-case all elements in monotonic order). Uses O(n) in worst case overall (each element stored once). Bounded by window length k (or n total)

Amortised O(1) means that although a single push might do multiple pops, each element can cause at most one pop, so across the whole sequence the average cost per operation stays constant. Monotonic queues thus combine the output sensitivity of a deque with the query power of a heap. Use this cheat-sheet as a quick reference when implementing or analyzing a monotonic queue structure.

Further Reading

algorithms data-structures monotonic-queues