Monotonic Data Structures
Monotonic Stack for Next Greater / Previous Smaller Element in linear time, Largest Rectangle in Histogram, and Monotonic Queue for sliding windows.
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.