Pattern 14 · Stacks

Monotonic Stack

A stack that keeps its elements in sorted order — and pops anything that would break it. Every pop answers a "next greater / next smaller" question, and since each element is pushed and popped at most once, the whole thing is one amortized O(n) pass.

time O(n)space O(n)variants increasing · decreasingpractice 11 problems
01 core concept

Pop until the invariant holds

A monotonic stack maintains elements in strictly increasing or decreasing order. When a new element violates the order, we pop elements until the invariant is restored — and the moment an element is popped, it has found its answer: the arriving element is its "next greater" (or "next smaller").

Decreasing monotonic stack processing [2, 1, 5, 6, 2, 3]: Stack state at each step (top on right): Element 2: push Stack: [2] Element 1: push Stack: [2, 1] (1 < 2, order maintained) Element 5: pop 1, Stack: [2] (5 > 1, pop! 1's next greater = 5) pop 2, Stack: [] (5 > 2, pop! 2's next greater = 5) push 5 Stack: [5] Element 6: pop 5, Stack: [] (6 > 5, pop! 5's next greater = 6) push 6 Stack: [6] Element 2: push Stack: [6, 2] (2 < 6, order maintained) Element 3: pop 2, Stack: [6] (3 > 2, pop! 2's next greater = 3) push 3 Stack: [6, 3] End: remaining in stack have no next greater element.
1 / 9walkthrough — next greater element on [2, 1, 2, 4, 3]
2
1
2
4
3
stack (idx) []
result [-1,-1,-1,-1,-1]
Goal: for each element, the next element to its right that is greater. The stack holds indices whose answer is still unknown — values decreasing from bottom to top.
02 two variants

Which way does the stack lean?

V1Decreasing stack → next GREATER element

The stack keeps decreasing values (top is smallest). A larger arrival triggers pops — those popped elements just found their "next greater."

Array: [4, 2, 1, 5, 3] ↑ 5 pops 1, 2, 4 (all smaller) Stack evolution: [4] → [4,2] → [4,2,1] → 5 arrives! pop 1 (next greater = 5) pop 2 (next greater = 5) pop 4 (next greater = 5) push 5 → [5] → [5,3] Next greater: {4→5, 2→5, 1→5, 5→None, 3→None}

V2Increasing stack → next SMALLER element

The stack keeps increasing values (top is largest). A smaller arrival triggers pops — those popped elements just found their "next smaller."

Array: [3, 7, 8, 4, 2] ↑ 4 pops 8, 7 (both larger) Stack evolution: [3] → [3,7] → [3,7,8] → 4 arrives! pop 8 (next smaller = 4) pop 7 (next smaller = 4) push 4 → [3,4] → [3,4] → 2 arrives! pop 4 (next smaller = 2) pop 3 (next smaller = 2) push 2 → [2] Next smaller: {3→2, 7→4, 8→4, 4→2, 2→None}
The mnemonic
The stack rejects elements that found their answer. Want the next greater? Keep a decreasing stack. Want the next smaller? Keep an increasing stack.
03 recognition

When to reach for it

Signal in problemStack typeExample
"Next greater element"DecreasingDaily Temperatures
"Next smaller element"IncreasingNext Smaller Element
"Previous greater element"DecreasingStock Span
"Previous smaller element"IncreasingHistogram boundaries
"Largest rectangle in histogram"IncreasingLC 84
"Remove digits to make smallest"IncreasingRemove K Digits
"Trapping rain water"DecreasingLC 42 (stack method)
Key indicator
The problem asks about relationships between elements based on their relative sizes, looking left or right — "for each element, find the first thing bigger/smaller than it."

Strategy framework

  1. What relationship? Next / previous, greater / smaller.
  2. Choose the direction: next greater → decreasing stack; next smaller → increasing stack.
  3. Store indices, not values. Access values via arr[stack[-1]] — indices let you compute distances and widths.
  4. The answer is determined at pop time. The current element is the answer for every element it pops.
04 templates & classics

The templates to memorize

T1Next greater element

next_greater.pypython
def next_greater_element(nums):
    """For each element, find the next element that is greater."""
    n = len(nums)
    result = [-1] * n
    stack = []  # stores INDICES, maintains decreasing values

    for i in range(n):
        # Pop all smaller elements — current is their "next greater"
        while stack and nums[stack[-1]] < nums[i]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)

    return result

# nums =   [2, 1, 2, 4, 3]
# result = [4, 2, 4,-1,-1]   (traced step by step in the visualizer above)

T2Next smaller element

Identical shape — only the comparison flips.

next_smaller.pypython
def next_smaller_element(nums):
    """For each element, find the next element that is smaller."""
    n = len(nums)
    result = [-1] * n
    stack = []  # stores INDICES, maintains increasing values

    for i in range(n):
        while stack and nums[stack[-1]] > nums[i]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)

    return result

T3Daily Temperatures — answer is a distance

Classic next-greater, but the answer stored at pop time is i - idx — the distance, not the value. This is why the stack holds indices.

daily_temperatures.py — LC 739python
def dailyTemperatures(temperatures):
    """Days until a warmer temperature. Classic next greater element."""
    n = len(temperatures)
    result = [0] * n
    stack = []  # decreasing stack of indices

    for i in range(n):
        while stack and temperatures[stack[-1]] < temperatures[i]:
            idx = stack.pop()
            result[idx] = i - idx  # distance, not value
        stack.append(i)

    return result

# temps =   [73, 74, 75, 71, 69, 72, 76, 73]
# result =  [ 1,  1,  4,  2,  1,  1,  0,  0]
#
# i=0 (73): stack=[0]
# i=1 (74): 74>73 → pop 0, result[0]=1-0=1     stack=[1]
# i=2 (75): 75>74 → pop 1, result[1]=2-1=1     stack=[2]
# i=3 (71): 71<75                                stack=[2,3]
# i=4 (69): 69<71                                stack=[2,3,4]
# i=5 (72): 72>69 → pop 4, result[4]=5-4=1
#           72>71 → pop 3, result[3]=5-3=2      stack=[2,5]
# i=6 (76): 76>72 → pop 5, result[5]=6-5=1
#           76>75 → pop 2, result[2]=6-2=4      stack=[6]
# i=7 (73): 73<76                                stack=[6,7]
# Remaining: result[6]=0, result[7]=0

T4Circular array — iterate twice

next_greater_ii.py — LC 503python
def nextGreaterElements(nums):
    """Circular array: treat as if array is doubled."""
    n = len(nums)
    result = [-1] * n
    stack = []

    # Iterate through array TWICE (simulate circular)
    for i in range(2 * n):
        idx = i % n
        while stack and nums[stack[-1]] < nums[idx]:
            result[stack.pop()] = nums[idx]
        if i < n:  # only push indices from first pass
            stack.append(i)

    return result

# nums = [1, 2, 1]  →  conceptually [1, 2, 1, | 1, 2, 1]
# First pass:  normal next greater
# Second pass: wrap around to find answers for remaining
# result = [2, -1, 2]   (1→2, 2→none since it's the max, 1→2 wrapping)

T5Largest Rectangle in Histogram

Increasing stack. When a shorter bar arrives, every popped bar knows both its boundaries: the arriving bar is its first shorter bar on the right, and the new stack top is its first shorter bar on the left.

heights = [2, 1, 5, 6, 2, 3] 6 | ___ 5 | ___ |xxx| 4 | |xxx||xxx| Key: xxx = largest rectangle 3 | |xxx||xxx| ___ (height 5, width 2, area 10) 2 |___||xxx||xxx| | | 1 ||___||xxx||xxx|__|___| 0 |_0____1____2____3____4____5_ 2 1 5 6 2 3 Bar at index 2 (height 5): Left boundary: index 1 (height 1 < 5) Right boundary: index 4 (height 2 < 5) Width: 4 - 1 - 1 = 2 → Area: 5 * 2 = 10
largest_rectangle.py — LC 84python
def largestRectangleArea(heights):
    """Find the largest rectangle that fits under the histogram."""
    stack = []  # increasing stack of indices
    max_area = 0
    heights.append(0)  # sentinel to flush remaining bars

    for i in range(len(heights)):
        while stack and heights[stack[-1]] > heights[i]:
            h = heights[stack.pop()]
            # Width: from current i to the new stack top
            w = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, h * w)
        stack.append(i)

    heights.pop()  # restore input
    return max_area

# heights = [2, 1, 5, 6, 2, 3]
#
# i=0(2): push          stack=[0]
# i=1(1): pop 0, h=2, w=1, area=2    stack=[1]
# i=2(5): push          stack=[1,2]
# i=3(6): push          stack=[1,2,3]
# i=4(2): pop 3, h=6, w=4-2-1=1, area=6
#         pop 2, h=5, w=4-1-1=2, area=10  ← MAX
#         push           stack=[1,4]
# i=5(3): push          stack=[1,4,5]
# sentinel(0): pop 5, h=3, w=6-4-1=1, area=3
#              pop 4, h=2, w=6-1-1=4, area=8
#              pop 1, h=1, w=6, area=6
#
# Answer: 10 (bar of height 5, width 2)

T6Remove K Digits — greedy build

An increasing stack builds the smallest number: any digit larger than the arriving one is dead weight, so pop it (while removals remain).

remove_k_digits.py — LC 402python
def removeKdigits(num, k):
    """Remove k digits to make the smallest possible number."""
    stack = []  # increasing stack (build smallest number)

    for digit in num:
        while k > 0 and stack and stack[-1] > digit:
            stack.pop()
            k -= 1
        stack.append(digit)

    # If k removals remain, remove from the end
    stack = stack[:len(stack) - k]

    # Remove leading zeros and handle empty result
    return "".join(stack).lstrip("0") or "0"

# num = "1432219", k = 3
#
# '1': stack=[1]
# '4': stack=[1,4]
# '3': 3<4, pop 4 (k=2), stack=[1,3]
# '2': 2<3, pop 3 (k=1), stack=[1,2]
# '2': stack=[1,2,2]
# '1': 1<2, pop 2 (k=0), stack=[1,2,1]
# '9': stack=[1,2,1,9]
#
# k=0, done. Result: "1219"

T7Trapping Rain Water — layer by layer

trap.py — LC 42, stack approachpython
def trap(height):
    """Calculate trapped rainwater using a monotonic stack."""
    stack = []  # decreasing stack of indices
    water = 0

    for i in range(len(height)):
        while stack and height[i] > height[stack[-1]]:
            bottom = height[stack.pop()]
            if not stack:
                break
            # Water trapped between stack[-1] and i, above bottom
            w = i - stack[-1] - 1
            h = min(height[stack[-1]], height[i]) - bottom
            water += w * h
        stack.append(i)

    return water
height = [0,1,0,2,1,0,1,3,2,1,2,1] ___ ___ | |___ ___ ___ ~~~ | |___ ~~~ | |___| |___ __|___|_~~~~_|_______|_~~~~_|___________________|__ 0 1 0 2 1 0 1 3 2 1 2 1 Water trapped (shown as ~~~): 6 units

T8Stock Span — previous greater element

Here the answer is computed at push time instead of pop time: after clearing everything ≤ today's price, the stack top is the previous greater element, and the span is the distance to it.

stock_span.py — LC 901python
def stock_span(prices):
    """For each day, how many consecutive days price was <= today's price."""
    n = len(prices)
    span = [0] * n
    stack = []  # decreasing stack of indices

    for i in range(n):
        while stack and prices[stack[-1]] <= prices[i]:
            stack.pop()
        # Span = distance to previous greater element (or start)
        span[i] = i + 1 if not stack else i - stack[-1]
        stack.append(i)

    return span

# prices = [100, 80, 60, 70, 60, 75, 85]
# span   = [  1,  1,  1,  2,  1,  4,  6]
#
# Day 5 (75): previous greater is day 1 (80), span = 5-1 = 4
# Day 6 (85): previous greater is day 0 (100), span = 6-0 = 6
05 complexity

Everything is O(n)

ProblemTimeSpaceNotes
Next Greater ElementO(n)O(n)Each element pushed/popped once
Next Smaller ElementO(n)O(n)Same as above
Daily TemperaturesO(n)O(n)Next greater variant
Next Greater Element IIO(n)O(n)2n iterations, still O(n)
Largest Rectangle (Histogram)O(n)O(n)Finding left/right bounds
Remove K DigitsO(n)O(n)Single pass
Trapping Rain Water (stack)O(n)O(n)Layer-by-layer computation
Stock SpanO(n)O(n)Previous greater element
Why the nested while loop is still O(n)
Each element is pushed at most once and popped at most once across the entire algorithm — at most 2n stack operations total, so the inner loop is amortized O(1) per step. Explaining this amortized analysis is itself a common interview question.
06 common mistakes

Where stacks go wrong

Mistake 1 — storing values instead of indices
Always store indices. You can look up values with nums[stack[-1]], but indices give you the positional information needed for distances and widths — values alone can't.
Mistake 2 — wrong stack direction
Next GREATER → decreasing stack (pop smaller). Next SMALLER → increasing stack (pop larger). Mnemonic: the stack rejects elements that found their answer.
Mistake 3 — forgetting the sentinel in histogram problems
Without appending a 0 at the end, bars remaining in the stack never get processed. Either add a sentinel or handle the leftovers after the loop.
Mistake 4 — off-by-one in width calculation
For histogram rectangles: width = i if not stack else i - stack[-1] - 1. The -1 accounts for the boundary bar not being included.
Mistake 5 — confusing "next" vs "previous"
For NEXT greater, the answer lands at pop time (iterating left to right). For PREVIOUS greater, compute at push time (stack top after clearing) — or iterate right to left.
Mistake 6 — circular array handling
Iterate 2 * n times using i % n, but only push indices during the first pass (if i < n) — otherwise you double-push everything.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

11 problems
easyLC 496
Hash map + decreasing stack. The warm-up.
mediumLC 739
Next greater — the answer is a distance, not a value.
mediumLC 503
Circular array — iterate 2n with i % n.
mediumLC 901
Previous greater element — answer at push time.
mediumLC 402
Increasing stack + greedy digit removal.
mediumLC 907
Next + previous smaller element per index.
mediumLC 316
Increasing stack + seen set + remaining counts.
mediumLC 456
Decreasing stack scanned from the right.
hardLC 84
Increasing stack + boundary widths + sentinel.
hardLC 85
Row-by-row histogram — LC 84 applied per row.
hardLC 42
Decreasing stack, water computed layer by layer.