DevLift
Back to Blog

Monotonic Stack: Next Greater Element and Daily Temperatures

A monotonic stack maintains elements in order and pops when that order breaks — finding the next greater element for every popped value in O(n) total.

Admin
August 3, 20265 min read10 views

Monotonic Stack: Next Greater Element and Daily Temperatures

The monotonic stack is one of those patterns that looks like a trick until you internalize the invariant. Once you do, you see it everywhere — next greater element, daily temperatures, largest rectangle in histogram, trapping rain water. They all reduce to the same underlying question: "for each element, when does the next something-bigger arrive?"

A monotonic decreasing stack maintains elements in descending order from bottom to top. Whenever you push something larger than the top, you pop until the invariant holds again. Each pop is the moment you've found the "next greater element" for the popped value.

The Core Invariant

Push onto the stack. If the new element is greater than the top, the top's "next greater" has arrived — pop and record the answer. Repeat until the stack is empty or the top is greater than the new element. Then push.

Rendering diagram...

After processing all elements, anything left in the stack has no next greater element — they never got popped by something larger.

Next Greater Element I (LeetCode 496)

Given two arrays nums1 and nums2, find for each element in nums1 its next greater element in nums2.

Brute force — nested loops:

// For each element in nums1, scan right in nums2 to find NGE
// Time: O(n * m) — for each of n elements, scan up to m positions
// Space: O(1) excluding output
const nextGreaterElementBrute = (nums1, nums2) => {
  return nums1.map((target) => {
    let found = false;
    for (const num of nums2) {
      if (num === target) found = true;
      if (found && num > target) return num;
    }
    return -1;
  });
};

This is O(n * m). The monotonic stack preprocesses nums2 in O(m) so each nums1 lookup is O(1).

Monotonic stack solution:

// Precompute NGE for all of nums2, then answer nums1 in O(1) per query
// Time: O(n + m) — one pass through nums2, one lookup pass through nums1
// Space: O(m) for the map and stack
const nextGreaterElementStack = (nums1, nums2) => {
  const ngeMap = new Map(); // element → its next greater element in nums2
  const stack = [];         // monotonic decreasing stack
 
  for (const num of nums2) {
    // Pop everything smaller than num — num is their NGE
    while (stack.length && stack[stack.length - 1] < num) {
      ngeMap.set(stack.pop(), num);
    }
    stack.push(num);
  }
  // Remaining stack elements have no NGE
  while (stack.length) ngeMap.set(stack.pop(), -1);
 
  // `?? -1` matters: LeetCode guarantees nums1 is a subset of nums2, but without
  // the fallback an element that isn't in nums2 yields `undefined`, not -1.
  return nums1.map((num) => ngeMap.get(num) ?? -1);
};

The while (stack.length && stack[stack.length - 1] < num) loop is the core. Read it as: "as long as the top of the stack is waiting for something greater, and num is greater — satisfy it."

Store values in the stack when elements are unique (like this problem). Store indices when you need positions, spans, or the original array to recheck values.

Daily Temperatures (LeetCode 739)

Given an array of daily temperatures, return an array where each element is the number of days until a warmer temperature. If no warmer day exists, use 0.

This is the same pattern but indexed — you need the distance between positions, not just the next value.

Brute force:

// For each day, scan forward to find the next warmer day
// Time: O(n^2) — nested loops
// Space: O(1) excluding output
const dailyTemperaturesBrute = (temps) => {
  const result = new Array(temps.length).fill(0);
  for (let i = 0; i < temps.length; i++) {
    for (let j = i + 1; j < temps.length; j++) {
      if (temps[j] > temps[i]) {
        result[i] = j - i;
        break;
      }
    }
  }
  return result;
};

Monotonic stack with indices:

// Stack holds indices, not values — we need distance between positions
// Time: O(n) — each index pushed and popped at most once
// Space: O(n) — stack holds at most n indices
const dailyTemperaturesStack = (temps) => {
  const result = new Array(temps.length).fill(0);
  const stack = []; // indices, stored in decreasing temperature order
 
  for (let i = 0; i < temps.length; i++) {
    // Pop all indices with temperature less than today's
    while (stack.length && temps[stack[stack.length - 1]] < temps[i]) {
      const prevIdx = stack.pop();
      result[prevIdx] = i - prevIdx; // Days waited = current index - previous index
    }
    stack.push(i);
  }
 
  return result; // Indices still in stack get 0 (default) — no warmer day found
};

Note the stack stores indices here because the answer requires i - prevIdx. You look up the temperature via temps[stack[stack.length - 1]]. This is the standard pattern for distance-based problems.

⚠️
When the stack stores indices, always look up the temperature via the original array: temps[stack[stack.length - 1]]. A common bug is storing the temperature in the stack and then losing the index needed to compute the distance.

Online Stock Span (LeetCode 901)

Design a class that collects stock prices and returns the span — the number of consecutive days (including today) with a price less than or equal to today's.

This is the "look backward" version of the same pattern. Instead of scanning right for something bigger, you're counting how far left you can go while current stays biggest.

// Stack stores [price, span] pairs
// Time: O(1) amortized — each element pushed and popped at most once
// Space: O(n) — stack at most n deep
class StockSpanner {
  constructor() {
    this.stack = []; // [price, span] pairs
  }
 
  next(price) {
    let span = 1;
 
    // Merge spans of all previous days with price <= today's
    while (this.stack.length && this.stack[this.stack.length - 1][0] <= price) {
      span += this.stack.pop()[1]; // Absorb the span of the popped day
    }
 
    this.stack.push([price, span]);
    return span;
  }
}

The key: when you pop a [price, span] pair, you absorb its span into the current span. You don't need to re-examine all those individual days — their span is already stored compactly.

Example trace for prices [100, 80, 60, 70, 60, 75, 85]. Feeding those seven prices through the class above returns spans [1, 1, 1, 2, 1, 4, 6], and the stack just before 85 arrives is [[100,1], [80,1], [75,4]] — only three entries, because 75 already absorbed the 60 and 70 behind it. So:

  • 85 pops [75, 4] and then [80, 1], stops at [100, 1], and returns 1 + 4 + 1 = 6

That compression is the whole trick. The three days 60, 70, 60 never get re-examined; their work is already folded into the single [75, 4] entry.

Rendering diagram...

The Three Variants at a Glance

// Variant 1: Find next greater VALUE (store values)
for (const num of nums) {
  while (stack.length && stack[stack.length - 1] < num) {
    ngeMap.set(stack.pop(), num);
  }
  stack.push(num);
}
 
// Variant 2: Find next greater by DISTANCE (store indices)
for (let i = 0; i < arr.length; i++) {
  while (stack.length && arr[stack[stack.length - 1]] < arr[i]) {
    const j = stack.pop();
    result[j] = i - j; // Distance to next greater
  }
  stack.push(i);
}
 
// Variant 3: Count span looking BACKWARD (store [value, span])
let span = 1;
while (this.stack.length && this.stack[this.stack.length - 1][0] <= price) {
  span += this.stack.pop()[1];
}
this.stack.push([price, span]);

The decreasing order invariant stays the same across all three. What changes is what you store (value, index, or compressed span) and the direction you think about the problem (forward vs. backward).

Interview Tips

State the invariant first. "I'll use a monotonic decreasing stack — I'll maintain elements in decreasing order and pop whenever I see something larger." This immediately signals you know the pattern, not just a solution for this specific problem.

Indices vs. values: If you need positions (distances, spans), store indices. If you only need values (the NGE itself), store values. Storing indices is almost always the safer default since you can always recover the value via arr[idx].

The amortized complexity argument: Each element is pushed once and popped at most once → O(n) total work even though the inner while loop runs multiple times. Be ready to explain this — interviewers sometimes mistake the nested loops for O(n²).

It's worth checking the claim rather than asserting it. On random arrays of 100,000 temperatures in the LeetCode range 30–100 (Node 22.22.3), across five runs dailyTemperaturesStack took 1.5–2.8 ms against 41.6–45.0 ms for the nested-loop version. Call it 20x here — the useful part isn't the ratio, it's that the ratio grows with n, because one side is linear and the other isn't. Both functions returned identical output across 2,500 random arrays plus the empty, single-element, all-equal, strictly-increasing and strictly-decreasing cases.

Decreasing vs. increasing stack:

  • Next greater element → decreasing stack (pop when you see something larger)
  • Next smaller element → increasing stack (pop when you see something smaller)
  • Largest Rectangle in Histogram → increasing stack (pop when the bar gets shorter)
  • Trapping Rain Water → also solvable with monotonic stack, though two-pointer is more common

Follow-up to expect: Largest Rectangle in Histogram (LC 84) uses the same pop-on-smaller invariant but needs careful handling of the "remaining stack at end" case. That problem is worth practicing as a harder extension.

Comments (0)

No comments yet. Be the first to share your thoughts!

Related Articles

The EventEmitter pattern lets components in the same process react to the same event without being directly coupled — no message broker needed.
AdminAugust 3, 20266 min read
One file to guard every route — plus the Next.js 16 rename that moves it off the Edge runtime, the request-vs-response header trap that leaks user IDs to the browser, and the CVE that explains why this can never be your only auth layer.
AdminAugust 3, 20268 min read
Parallel routes let a single layout render multiple independent pages at once. Combine them with intercepting routes and you get URL-aware modals with zero hacks.
AdminAugust 3, 20267 min read