Skip to content

05 · Monotonic Stack & Queue

A monotonic stack is an ordinary stack with one rule: its contents stay sorted (increasing or decreasing) from bottom to top. Before pushing a new element, you pop everything that would break the order — and the moment an element is popped is the moment you learn its answer.

Signals: "next greater / next smaller element", "previous smaller", "how many days until warmer", "span", "largest rectangle", "remove k digits to make the smallest number", "sliding window maximum". Any time a brute force scans left or right from each element looking for the first element that beats it.

Worked problem 1: next greater element

Problem. For each element, find the next element to its right that is strictly greater, or -1.

Approach. Scan left to right, keeping a stack of indices whose answer is still unknown. Their values are decreasing from bottom to top (if a bigger element had come after a smaller one, the smaller one would already have its answer). When a new value arrives, it is the answer for every stacked index with a smaller value.

def next_greater(nums):
    result = [-1] * len(nums)
    stack = []                               # indices, values decreasing
    for i, x in enumerate(nums):
        while stack and nums[stack[-1]] < x:
            result[stack.pop()] = x
        stack.append(i)
    return result


assert next_greater([2, 1, 2, 4, 3]) == [4, 2, 4, -1, -1]
assert next_greater([5, 4, 3]) == [-1, -1, -1]           # decreasing: no answers
assert next_greater([1, 2, 3]) == [2, 3, -1]
assert next_greater([]) == []
assert next_greater([2, 2]) == [-1, -1]                  # equal is not greater

Complexity: O(n) — each index is pushed once and popped at most once.

Daily temperatures

Same pattern, but the answer is the distance to the next warmer day:

def daily_temperatures(temps):
    result = [0] * len(temps)
    stack = []
    for i, t in enumerate(temps):
        while stack and temps[stack[-1]] < t:
            j = stack.pop()
            result[j] = i - j
        stack.append(i)
    return result


assert daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]) == [1, 1, 4, 2, 1, 1, 0, 0]
assert daily_temperatures([30]) == [0]

Circular arrays

For "next greater in a circular array", loop over indices 0 .. 2n-1 using i % n, and only push during the first pass. Each element gets a second chance to see elements that come before it.

def next_greater_circular(nums):
    n = len(nums)
    result = [-1] * n
    stack = []
    for i in range(2 * n):
        x = nums[i % n]
        while stack and nums[stack[-1]] < x:
            result[stack.pop()] = x
        if i < n:
            stack.append(i)
    return result


assert next_greater_circular([1, 2, 1]) == [2, -1, 2]
assert next_greater_circular([3, 3]) == [-1, -1]

Worked problem 2: largest rectangle in a histogram

Problem. Bars of width 1 with heights h. Find the area of the largest rectangle inside the histogram.

Insight. For each bar, the widest rectangle using that bar's full height extends left and right until the first shorter bar on each side. An increasing stack gives both boundaries: when bar j is popped by a shorter bar i, the right boundary is i, and the left boundary is whatever is below j on the stack.

def largest_rectangle(heights):
    stack = []                           # indices, heights increasing
    best = 0
    for i, h in enumerate(heights + [0]):     # sentinel 0 flushes the stack at the end
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            left = stack[-1] if stack else -1
            best = max(best, height * (i - left - 1))
        stack.append(i)
    return best


assert largest_rectangle([2, 1, 5, 6, 2, 3]) == 10     # heights 5 and 6, width 2
assert largest_rectangle([2, 4]) == 4
assert largest_rectangle([]) == 0
assert largest_rectangle([3, 3, 3]) == 9               # equal heights
assert largest_rectangle([1, 2, 3, 4, 5]) == 9         # 3 * 3

heights + [0] creates a copy with the sentinel; heights[stack[-1]] never reads the sentinel's index while it is on the stack, because the loop ends right after pushing it.

Complexity: O(n). This problem is the core of "maximal rectangle of 1s in a matrix": treat each row as the base of a histogram of consecutive-1 heights.

Worked problem 3: sliding window maximum (monotonic deque)

Problem. Return the maximum of every window of size k.

Approach. Keep a deque of indices whose values are decreasing from front to back. The front is always the current window's maximum. When a new value arrives, pop smaller values from the back (they can never be a maximum while this larger, newer value is in the window). Pop the front when it slides out of the window.

from collections import deque

def max_sliding_window(nums, k):
    dq, out = deque(), []
    for i, x in enumerate(nums):
        while dq and nums[dq[-1]] <= x:
            dq.pop()                          # dominated: older and not larger
        dq.append(i)
        if dq[0] <= i - k:
            dq.popleft()                      # left the window
        if i >= k - 1:
            out.append(nums[dq[0]])
    return out


assert max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3) == [3, 3, 5, 5, 6, 7]
assert max_sliding_window([1], 1) == [1]
assert max_sliding_window([9, 8, 7], 1) == [9, 8, 7]      # edge: k = 1
assert max_sliding_window([4, 2, 12, 3], 4) == [12]       # edge: k = n

O(n) time, O(k) space. A heap would give O(n log n); the deque is strictly better here.

Worked problem 4: remove k digits

Problem. Remove k digits from a number string to make the smallest possible number.

Greedy + monotonic stack. Scan left to right. While the previous kept digit is larger than the current one and removals remain, remove it — a larger digit in a more significant position is always worse.

def remove_k_digits(num, k):
    stack = []
    for d in num:
        while k and stack and stack[-1] > d:
            stack.pop()
            k -= 1
        stack.append(d)
    if k:
        stack = stack[:-k]                  # remaining removals from the end
    return "".join(stack).lstrip("0") or "0"


assert remove_k_digits("1432219", 3) == "1219"
assert remove_k_digits("10200", 1) == "200"
assert remove_k_digits("10", 2) == "0"                  # edge: remove everything
assert remove_k_digits("12345", 2) == "123"             # increasing: trim the end

How It Actually Works

Amortized O(n). The while loop inside the for loop looks quadratic, but every index is pushed exactly once and popped at most once over the whole run. So the total number of pops is at most n, and the algorithm is O(n) overall — the same aggregate argument as the sliding window in Level 1.

Why popping is safe (the dominance argument). In the next-greater problem, an index j stays on the stack only while no greater element has appeared after it. When x arrives and nums[j] < x, x is by definition the first greater element after j (anything between them was ≤ nums[j], or j would already have been popped). So j's answer is final and j can leave. In the sliding-window maximum, an index is popped from the back when a newer, larger value arrives: the older value leaves the window earlier and is smaller, so it can never be the maximum again. Removing elements that are provably useless is what keeps the structure small and sorted.

What the stack encodes. At any moment, a decreasing stack holds exactly the "candidates still waiting for a greater element", and they are sorted because each one survived all the elements pushed after it. For the histogram, the element below a popped bar in an increasing stack is the nearest shorter bar to its left — which is why one structure yields both boundaries.

Common mistakes

  • Storing values instead of indices when you need distances or window boundaries.
  • Using < vs <= inconsistently — decide how equal elements should behave (strictly greater? equal allowed?) and test with duplicates.
  • Forgetting to flush the stack at the end (use a sentinel or a final loop).
  • In the sliding-window deque, removing the front with a value comparison instead of an index comparison (fails with duplicate values).
  • Missing lstrip("0") or "0" in remove k digits.

Variations to practice

  • Online stock span (previous greater-or-equal, streaming).
  • Sum of subarray minimums (previous smaller and next smaller for each element).
  • Maximal rectangle in a binary matrix.
  • Trapping rain water with a stack.
  • Shortest subarray with sum at least k (monotonic deque over prefix sums; works with negatives).
  • Remove duplicate letters (smallest subsequence with each letter once).

Exercise

Solve Sum of Subarray Minimums: for every contiguous subarray, take its minimum, and return the sum of those minimums. For each index i, find how many subarrays have nums[i] as their minimum using the distance to the previous smaller element and to the next smaller-or-equal element (asymmetric to avoid double-counting duplicates). Test with [3,1,2,4] → 17, [11,81,94,43,3] → 444, [2,2] → 6, and a single element.