Monotonic Data Structures
Monotonic Stack for Next Greater / Previous Smaller Element in linear time, Largest Rectangle in Histogram, and Monotonic Queue for sliding windows.
Reinforce your mental model: run, pause, and inspect live pointers with our interactive visualizer:
Monotonic stacks and queues enforce strict ordering invariants over dynamically filtered sequences, pruning obsolete candidates in amortized time per element. From range extrema lookups and histogram geometry to sliding-window signal filtering, monotonic structures convert intractable and lookups into optimal linear scans.
1. Executive Summary & Learning Objectives
#This module explores ordered linear containers that discard dominated elements as new candidates arrive, maintaining a monotonic sub-sequence that guarantees immediate access to extreme values.
By the end of this chapter, you will be able to:
- Enforce Monotonic Stack Invariants: Implement increasing and decreasing stacks to resolve Next/Previous Greater/Smaller Element queries in amortized time.
- Maximize Geometric Subarrays: Apply monotonic stacks to find the largest rectangular area in a histogram by tracking span boundaries in time.
- Construct Monotonic Deques: Maintain double-ended queues of indices to solve the Sliding Window Maximum problem in strictly time and auxiliary space.
- Prove Amortized Invariants: Mathematically demonstrate why inner while loops across monotonic structures achieve aggregate time through single-push, single-pop guarantees.
2. Topic 139: Monotonic Stack Pattern
#1. Definition & Core Invariant
#A Monotonic Stack is a last-in, first-out container that maintains its stored elements in strictly sorted order (either monotonically increasing or decreasing) from bottom to top:
| Stack Type | Bottom-to-Top Invariant | Popping Condition on Incoming | Primary Query Resolved |
|---|---|---|---|
| Monotonically Increasing | Elements increase: | Pop while | Next/Previous Smaller Element |
| Monotonically Decreasing | Elements decrease: | Pop while | Next/Previous Greater Element |
2. Next Greater Element (NGE) Problem Formulation
#Given array , determine the first element to the right that is strictly greater than each element. If none exists, assign .
- Brute Force: Evaluate all pairs where time.
- Monotonic Stack Optimization: Scan right-to-left, popping elements smaller than or equal to linear time.
/**
* Computes Next Greater Element for every array position.
* Time Complexity: O(n) amortized
* Space Complexity: O(n) auxiliary stack
*/
export function nextGreaterElement(nums: number[]): number[] {
const n = nums.length;
const nge = new Array<number>(n);
const stack: number[] = []; // Monotonically decreasing stack storing values
// Traverse right-to-left
for (let i = n - 1; i >= 0; i--) {
// Discard elements dominated by nums[i]
while (stack.length > 0 && stack[stack.length - 1] <= nums[i]) {
stack.pop();
}
nge[i] = stack.length === 0 ? -1 : stack[stack.length - 1];
stack.push(nums[i]);
}
return nge;
}3. Step-by-Step State Trace:
#| Index | Element | Stack Before Action | Popped Elements | Assigned nge[i] | Stack After Push |
|---|---|---|---|---|---|
| 4 | 3 | [] | None | -1 | [3] |
| 3 | 4 | [3] | Pop 3 | -1 | [4] |
| 2 | 2 | [4] | None () | 4 | [4, 2] |
| 1 | 1 | [4, 2] | None () | 2 | [4, 2, 1] |
| 0 | 2 | [4, 2, 1] | Pop 1, Pop 2 | 4 | [4, 2] |
Final Result: nge = [4, 2, 4, -1, -1].
4. Amortized Complexity Proof
#Although an inner while loop executes during iterations, consider the aggregate lifecycle of elements:
- Each index is pushed onto the stack exactly once.
- Each index is popped from the stack at most once.
- The total number of stack operations across the entire algorithm cannot exceed .
- Therefore, the amortized cost per element is , yielding total runtime .
5. Signature Master Problem: Largest Rectangle in Histogram
#Given bar heights , compute the maximum rectangular area formed under the histogram bars.
For each bar , the maximum rectangle with height spans from its Previous Smaller Element (PSE) to its Next Smaller Element (NSE):
export function largestRectangleArea(heights: number[]): number {
const stack: number[] = []; // Stores indices
let maxArea = 0;
const n = heights.length;
for (let i = 0; i <= n; i++) {
const currentHeight = i === n ? 0 : heights[i];
while (stack.length > 0 && currentHeight < heights[stack[stack.length - 1]]) {
const height = heights[stack.pop()!];
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}3. Topic 140: Monotonic Queue Pattern
#1. Context: Sliding Window Maximum
#Given array of size and window size , find the maximum value in every contiguous window of length as it shifts right by 1 step.
| Algorithmic Approach | Time Complexity | Auxiliary Space | Bottleneck / Limitation |
|---|---|---|---|
| Brute Force Subarray Scan | Repeated redundant comparisons across overlapping intervals | ||
| Max-Heap (Priority Queue) | Lazy deletion requires heap pruning overhead | ||
| Monotonic Deque | Optimal aggregate amortized performance |
2. The Deque Invariant
#A Monotonic Queue maintains array indices in a double-ended queue (deque) adhering to two simultaneous invariants:
- Value Monotonicity: Indices in the deque correspond to strictly decreasing array values from front to back (). Consequently,
deque.front()always holds the index of the window maximum. - Window Expiration: If the front index satisfies , it has fallen out of window boundaries and is removed from the front.
- Domination Pruning: Before inserting index , all back indices where are popped. If an older element is smaller than a newer element, it can never serve as a window maximum.
3. Step-by-Step Sliding Window Trace: on
#| Current Index | Incoming | Window Bounds | Front Expirations | Back Removals (Dominated) | Deque State (Indices) | Window Max () |
|---|---|---|---|---|---|---|
| 0 | 1 | None | None | [0] | — | |
| 1 | 3 | None | Pop back | [1] | — | |
| 2 | -1 | None | None | [1, 2] | 3 () | |
| 3 | -3 | None | None | [1, 2, 3] | 3 () | |
| 4 | 5 | Pop front | Pop back | [4] | 5 () | |
| 5 | 3 | None | None | [4, 5] | 5 () | |
| 6 | 6 | None | Pop back | [6] | 6 () | |
| 7 | 7 | None | Pop back | [7] | 7 () |
4. Implementation: Sliding Window Maximum
#export function maxSlidingWindow(nums: number[], k: number): number[] {
const n = nums.length;
if (n === 0 || k === 0) return [];
const deque: number[] = []; // Stores indices
const result: number[] = [];
for (let i = 0; i < n; i++) {
// 1. Remove indices outside current window
if (deque.length > 0 && deque[0] <= i - k) {
deque.shift();
}
// 2. Remove indices with values dominated by nums[i]
while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) {
deque.pop();
}
// 3. Append current element index
deque.push(i);
// 4. Record front value once first window is complete
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}4. Module 02 Summary & Architectural Takeaways
#- Monotonic Stack: Solves range boundary queries (Next/Previous Greater/Smaller) in amortized time by discarding non-competitive elements.
- Histogram Geometry: The area under an irregular histogram decomposes into candidate rectangles defined by Previous and Next Smaller Element boundaries.
- Monotonic Deque: Maintains sliding window extrema in linear time by discarding elements that are both older and smaller than incoming candidates.
References & Academic Attribution
#- Halim, S., Halim, F., & Skiena, S. S. (2020). Competitive Programming 4: The Lower Bound of Programming Contests. CP4 Pte Ltd.
- Laaksonen, A. (2020). Guide to Competitive Programming: Learning and Improving Algorithms Through Contests (2nd ed.). Springer.
- Skiena, S. S. (2020). The Algorithm Design Manual (3rd ed.). Springer.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.). MIT Press.