Stacks & Monotonic Stacks
Use LIFO ordering to match pairs, track state, and evaluate expressions. Monotonic stacks extend the idea to efficiently find the next greater or smaller element in O(n) time.
Learn & Reference
Understanding the Pattern
Stacks & Monotonic Stacks Pattern
A stack is a Last-In, First-Out (LIFO) data structure. You push items onto the top and pop them from the top. This simple property makes stacks surprisingly powerful for a whole class of problems.
Core Concept
Think of a stack of plates: you can only add or remove from the top. This constraint is exactly what you need when the most recent item is the most relevant — matching brackets, undoing operations, or evaluating expressions in reverse order.
Basic Stack Applications
1. Matching & Pairing
The most classic stack problem: check if brackets/parentheses are balanced. When you see an opening bracket, push it. When you see a closing bracket, pop and check if they match. If the stack is empty at the end, everything matched.
This extends to any problem where you need to match nested pairs — HTML tags, directory paths, or nested function calls.
2. State Tracking (Auxiliary Stack)
Sometimes you need O(1) access to a running property (like the current minimum) while still supporting push/pop. The trick is to maintain a second "auxiliary" stack that tracks the property in sync with the main stack.
For example, a Min Stack keeps a parallel stack where each entry is the minimum of all elements at or below that position. When you push, you also push the new minimum. When you pop, you pop from both stacks.
3. Expression Evaluation
Stacks are the natural data structure for evaluating postfix (Reverse Polish Notation) expressions, infix expressions with operator precedence, and building/evaluating expression trees.
The pattern: push operands. When you encounter an operator, pop the required operands, compute, and push the result back.
Monotonic Stacks
A monotonic stack maintains its elements in either increasing or decreasing order. When you try to push an element that would violate the order, you pop elements until the invariant is restored.
Why Monotonic Stacks Work
The key insight: when an element gets popped by a new element, the new element is the next greater (or smaller) element for everything being popped. This turns an O(n²) "for each element, scan right" approach into O(n) total, because each element is pushed and popped at most once.
4. Next Greater / Smaller Element
Given an array, find the next greater element for each position. Iterate left to right, maintaining a monotonically decreasing stack of indices:
- For each new element, pop everything smaller — the new element is their "next greater"
- Push the current index
This is the foundation of monotonic stack problems. Variations include next smaller element (use increasing stack), previous greater/smaller (iterate right to left), and circular arrays.
5. Histogram / Rectangle Problems
The "largest rectangle in histogram" is the definitive monotonic stack problem. For each bar, you need the nearest shorter bar on the left and right. A monotonically increasing stack of indices gives you both boundaries:
- When a bar gets popped by a shorter bar, the popped bar's rectangle extends from the current stack top (left boundary) to the new bar (right boundary)
When Stacks Fail
- The problem requires random access (use an array or hash map instead)
- You need the minimum/maximum of a sliding window (use a monotonic deque, not a stack)
- The pairing is not nested (e.g., regex matching may need backtracking, not a stack)
- The ordering property is not monotonic (more complex data structures needed)
Common Mistakes
- Not processing remaining stack elements: After iterating through the array, elements left in a monotonic stack have no "next greater/smaller" — you must handle them (usually set to -1 or 0)
- Confusing increasing vs decreasing monotonic stack: A monotonically decreasing stack finds the next greater element. A monotonically increasing stack finds the next smaller element. This is counterintuitive
- Storing values instead of indices: For most monotonic stack problems, store indices (not values) so you can compute distances or positions. You can always look up the value from the original array
- Forgetting empty-stack checks: Always check if the stack is empty before peeking or popping — especially in bracket matching and monotonic stack problems
- Off-by-one in histogram problems: When computing widths in the histogram problem, the width is
current_index - stack_top - 1, notcurrent_index - popped_index. Getting this wrong is the #1 bug in histogram solutions
When to Use
Template
1# --- Basic Stack Usage ---2stack = []3stack.append(item) # push4top = stack[-1] # peek5item = stack.pop() # pop6if stack: # check not empty7# safe to peek/pop89# --- Monotonic Stack: Next Greater Element ---10def next_greater(nums):11n = len(nums)12result = [-1] * n13stack = [] # indices, maintaining decreasing values14for i in range(n):15# Pop elements smaller than current — current is their "next greater"16while stack and nums[i] > nums[stack[-1]]:17idx = stack.pop()18result[idx] = nums[i]19stack.append(i)20return result2122# --- Monotonic Stack: Next Smaller Element ---23def next_smaller(nums):24n = len(nums)25result = [-1] * n26stack = [] # indices, maintaining increasing values27for i in range(n):28while stack and nums[i] < nums[stack[-1]]:29idx = stack.pop()30result[idx] = nums[i]31stack.append(i)32return result
Syntax Reference
1# --- Python: list as stack ---2stack = [] # Create empty stack3stack.append(val) # Push: O(1) amortized4val = stack.pop() # Pop: O(1), raises IndexError if empty5top = stack[-1] # Peek: O(1), raises IndexError if empty6is_empty = len(stack) == 0 # Check empty: O(1)7size = len(stack) # Size: O(1)89# --- Safe peek/pop ---10if stack: # Check before peek/pop11top = stack[-1]12val = stack.pop()1314# --- collections.deque (alternative, O(1) from both ends) ---15from collections import deque16stack = deque()17stack.append(val) # Push right: O(1)18val = stack.pop() # Pop right: O(1)
Common Recipes
1# --- Bracket Matching ---2def is_valid(s):3stack = []4pairs = {')': '(', ']': '[', '}': '{'}5for ch in s:6if ch in '([{':7stack.append(ch)8elif not stack or stack.pop() != pairs[ch]:9return False10return len(stack) == 01112# --- Next Greater Element (Monotonic Decreasing Stack) ---13def next_greater(nums):14n = len(nums)15result = [-1] * n16stack = [] # stores indices17for i in range(n):18while stack and nums[i] > nums[stack[-1]]:19idx = stack.pop()20result[idx] = nums[i]21stack.append(i)22return result2324# --- Largest Rectangle in Histogram ---25def largest_rect(heights):26stack = [] # stores indices of increasing heights27max_area = 028for i, h in enumerate(heights):29start = i30while stack and stack[-1][1] > h:31idx, height = stack.pop()32max_area = max(max_area, height * (i - idx))33start = idx34stack.append((start, h))35for idx, height in stack:36max_area = max(max_area, height * (len(heights) - idx))37return max_area
Complexity
| Operation | Time | Space |
|---|---|---|
| Push / Pop / Peek | O(1) | O(1) |
| Bracket matching (n characters) | O(n) | O(n) |
| Next greater/smaller element (n elements) | O(n) | O(n) |
| Largest rectangle in histogram | O(n) | O(n) |
| Expression evaluation (n tokens) | O(n) | O(n) |
| Min Stack (push/pop/getMin) | O(1) each | O(n) |
Watch Out
- ✗Not processing remaining stack elements: After iterating through the array, elements left in a monotonic stack have no "next greater/smaller" — you must handle them (usually set to -1 or 0)
- ✗Confusing increasing vs decreasing monotonic stack: A monotonically *decreasing* stack finds the next greater element. A monotonically *increasing* stack finds the next smaller element. This is counterintuitive
- ✗Storing values instead of indices: For most monotonic stack problems, store indices (not values) so you can compute distances or positions. You can always look up the value from the original array
- ✗Forgetting empty-stack checks: Always check if the stack is empty before peeking or popping — especially in bracket matching and monotonic stack problems
- ✗Off-by-one in histogram problems: When computing widths in the histogram problem, the width is `current_index - stack_top - 1`, not `current_index - popped_index`. Getting this wrong is the #1 bug in histogram solutions