Monotonic Stack

intermediate monotonic-stack stack pattern

A monotonic stack is just a regular stack with one rule: the elements inside are always in sorted order (either increasing or decreasing from bottom to top). Whenever we push a new element, we pop off anything that violates this order.

In simple language, it’s a stack that stays sorted at all times. We enforce the ordering ourselves during push operations.

Why It Matters

Monotonic stacks turn O(n²) “find the next greater/smaller element” problems into O(n). That’s a massive improvement. They show up in a surprising number of interview problems: stock prices, temperatures, histograms, and more.

Two Flavors

Monotonically increasing (bottom to top): each element is larger than the one below it. We pop off elements that are greater than or equal to the new one.

Monotonically decreasing (bottom to top): each element is smaller than the one below it. We pop off elements that are less than or equal to the new one.

Monotonic Decreasing Stack -- Processing [3, 1, 4, 1, 5]
push 3:
3
push 1:
3 1
(1 < 3, ok)
push 4:
4
popped 1, 3 (both smaller than 4)
push 1:
4 1
(1 < 4, ok)
push 5:
5
popped 1, 4 (both smaller than 5)

The key insight: each element gets pushed once and popped at most once. So even though we have a while loop inside a for loop, the total work is O(n).

Pattern: Next Greater Element

We covered this briefly in the stacks topic. Let’s formalize the monotonic stack approach. We use a decreasing stack (from bottom to top) and iterate left to right.

When we’re about to push an element, everything we pop off has found its “next greater element” — it’s the element we’re about to push.

// Next greater element using monotonic stack -- O(n)
function nextGreaterElement(nums) {
  const result = new Array(nums.length).fill(-1);
  const stack = []; // stores indices, values decrease bottom to top

  for (let i = 0; i < nums.length; i++) {
    // pop all smaller elements -- current num is their "next greater"
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      result[stack.pop()] = nums[i];
    }
    stack.push(i);
  }
  return result;
}
// [2, 1, 3, 2, 4] -> [3, 3, 4, 4, -1]
# Next greater element using monotonic stack -- O(n)
def next_greater_element(nums):
    result = [-1] * len(nums)
    stack = []  # stores indices, values decrease bottom to top

    for i in range(len(nums)):
        # pop all smaller elements -- current num is their "next greater"
        while stack and nums[stack[-1]] < nums[i]:
            result[stack.pop()] = nums[i]
        stack.append(i)
    return result
# [2, 1, 3, 2, 4] -> [3, 3, 4, 4, -1]
// Next greater element using monotonic stack -- O(n)
static int[] nextGreaterElement(int[] nums) {
    int[] result = new int[nums.length];
    Arrays.fill(result, -1);
    Deque<Integer> stack = new ArrayDeque<>(); // stores indices

    for (int i = 0; i < nums.length; i++) {
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
            result[stack.pop()] = nums[i];
        }
        stack.push(i);
    }
    return result;
}
// [2, 1, 3, 2, 4] -> [3, 3, 4, 4, -1]

Daily Temperatures

Given daily temperatures, find how many days we need to wait for a warmer day. This is just “next greater element” but we return the distance instead of the value.

Example: [73, 74, 75, 71, 69, 72, 76, 73] returns [1, 1, 4, 2, 1, 1, 0, 0].

// Daily temperatures -- how many days until warmer?
function dailyTemperatures(temps) {
  const result = new Array(temps.length).fill(0);
  const stack = []; // stores indices of days waiting for a warmer day

  for (let i = 0; i < temps.length; i++) {
    while (stack.length && temps[stack[stack.length - 1]] < temps[i]) {
      const prevDay = stack.pop();
      result[prevDay] = i - prevDay; // distance to warmer day
    }
    stack.push(i);
  }
  return result;
}
# Daily temperatures -- how many days until warmer?
def daily_temperatures(temps):
    result = [0] * len(temps)
    stack = []  # stores indices of days waiting for a warmer day

    for i in range(len(temps)):
        while stack and temps[stack[-1]] < temps[i]:
            prev_day = stack.pop()
            result[prev_day] = i - prev_day  # distance to warmer day
        stack.append(i)
    return result
// Daily temperatures -- how many days until warmer?
static int[] dailyTemperatures(int[] temps) {
    int[] result = new int[temps.length];
    Deque<Integer> stack = new ArrayDeque<>();

    for (int i = 0; i < temps.length; i++) {
        while (!stack.isEmpty() && temps[stack.peek()] < temps[i]) {
            int prevDay = stack.pop();
            result[prevDay] = i - prevDay; // distance to warmer day
        }
        stack.push(i);
    }
    return result;
}

Notice how similar this is to the next greater element problem? Same pattern, different return value. That’s the beauty of monotonic stacks — once we know the pattern, many problems look the same.

Largest Rectangle in Histogram

This is a harder problem, but it’s a classic monotonic stack application. Given an array of bar heights, find the area of the largest rectangle we can form.

The key insight: for each bar, we need to know how far it can extend to the left and right without hitting a shorter bar. A monotonic increasing stack gives us exactly that.

// Largest rectangle in histogram -- O(n)
function largestRectangleArea(heights) {
  const stack = []; // stores indices, heights increase bottom to top
  let maxArea = 0;
  const n = heights.length;

  for (let i = 0; i <= n; i++) {
    const h = i === n ? 0 : heights[i]; // sentinel value at end

    while (stack.length && heights[stack[stack.length - 1]] > h) {
      const height = heights[stack.pop()];
      // width = distance between current i and new stack top
      const width = stack.length ? i - stack[stack.length - 1] - 1 : i;
      maxArea = Math.max(maxArea, height * width);
    }
    stack.push(i);
  }
  return maxArea;
}
# Largest rectangle in histogram -- O(n)
def largest_rectangle_area(heights):
    stack = []  # stores indices, heights increase bottom to top
    max_area = 0
    n = len(heights)

    for i in range(n + 1):
        h = heights[i] if i < n else 0  # sentinel value at end

        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            width = i - stack[-1] - 1 if stack else i
            max_area = max(max_area, height * width)
        stack.append(i)
    return max_area
// Largest rectangle in histogram -- O(n)
static int largestRectangleArea(int[] heights) {
    Deque<Integer> stack = new ArrayDeque<>();
    int maxArea = 0;
    int n = heights.length;

    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i]; // sentinel value at end

        while (!stack.isEmpty() && heights[stack.peek()] > h) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        stack.push(i);
    }
    return maxArea;
}

The sentinel value (0 at the end) forces us to process all remaining bars in the stack. Without it, we’d need a separate loop after the main iteration.

Choosing the Right Monotonic Stack

Problem TypeStack OrderDirection
Next greater elementDecreasing (bottom to top)Left to right
Next smaller elementIncreasing (bottom to top)Left to right
Previous greater elementDecreasingRight to left
Previous smaller elementIncreasingRight to left

When to Think Monotonic Stack

The signal is usually one of these:

  • “Find the next greater/smaller element”
  • “How many days until…”
  • “Largest rectangle / container”
  • Any problem where we need to efficiently look at nearby elements that satisfy a comparison

If a brute force approach uses nested loops where the inner loop searches for the next bigger/smaller value, that’s almost certainly a monotonic stack problem. We go from O(n²) to O(n) — each element is pushed and popped at most once.