Skip to content
Home DSA Monotonic Stack Pattern Fixes O(n²) Stock Indexing

Monotonic Stack Pattern Fixes O(n²) Stock Indexing

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Stack & Queue → Topic 5 of 10
30-sec timeout from O(n²) loops on 10k ticks.
🔥 Advanced — solid DSA foundation required
In this tutorial, you'll learn
30-sec timeout from O(n²) loops on 10k ticks.
  • A monotonic stack maintains sorted order, discarding elements that can never be useful.
  • Each element is pushed and popped at most once — total O(n) time for the entire algorithm.
  • Decreasing stack: pop when arr[new] > arr[top] → finds next greater element.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Declarative structure: maintain sorted order in a stack by popping violations on push
  • Two variants: decreasing (next greater) and increasing (next smaller)
  • Each element pushed and popped at most once → O(n) amortised
  • Real‑world: stock span APIs, histogram area, trapped rainwater
  • Production trap: wrong comparison sign (<= vs <) silently shifts answers
🚨 START HERE

Quick Debug Cheat Sheet: Monotonic Stack

Five‑second checks when your monotonic stack isn't working as expected.
🟡

Output array is empty or all `-1`

Immediate ActionPrint the stack after each iteration to ensure elements are being pushed/popped.
Commands
`print(f'index {i}: stack = {stack}')` inside the loop
`assert len(result) == len(arr)` after the loop
Fix NowIf stack stays empty, check your loop range: you may be iterating from 1 instead of 0.
🟡

Duplicate values break the logic

Immediate ActionRerun with a small duplicated dataset and trace the condition.
Commands
`trace_condition(arr[stack.top], arr[i])`
`print(f'pop {j} because {arr[j]} {op} {arr[i]}')`
Fix NowSwitch the condition sign (e.g., from `<` to `<=`) and rerun.
Production Incident

Stock Price Indexing Slowdown

A financial data pipeline processing 10M stock ticks per minute failed to meet SLA due to O(n^2) nested loops to compute next greater price. Replacing with monotonic stack cut latency 99%.
SymptomThe pipeline that computed next‑greater price for each tick (for option pricing) timed out after 30 seconds, causing backlog and missed market data windows.
AssumptionThe team assumed the tick volume was small enough for O(n²) — they didn't profile before deployment. They also thought a standard for loop inside a for loop was 'simple and correct'.
Root causeNested loops iterate over every pair of indices — O(n²) per batch of 10,000 ticks. With 10M ticks per minute, the algorithm never finished within the 500ms per batch window.
FixReplaced the nested loop with a monotonic decreasing stack that computes next greater element in O(n) total time. The change was localised to a single function and required no new dependencies.
Key Lesson
Always profile before assuming O(n²) is acceptable for variable input volume.Monotonic stack is a drop‑in replacement for many 'next greater/smaller' problems — it trades memory for massive time savings.Test with production data volumes in staging, not just with a handful of examples.
Production Debug Guide

Symptom‑driven checks for the three most common mistakes

All results are -1 (no greater/smaller found)Verify the comparison operator: for next greater you need arr[stack.top] < arr[i]; using <= treats equal values as not greater and may skip valid answers.
Answers are shifted by one index (off‑by‑one)Check that you pop the stack before pushing the current index. A common bug: pushing first, then popping — this causes the current element to never be compared.
Wrong result for duplicates (e.g., array with repeated values)Decide whether you need strict monotonic (< or >) or non‑strict (<= or >=). For Next Greater Element with duplicates, using <= gives the strictly greater — that's usually correct.

Most developers hit a wall with array problems that demand knowing the 'next greater' or 'previous smaller' element at every index. The naive approach — a nested loop — works on paper but collapses at scale. When you're processing stock prices, rendering bar charts, or evaluating weather forecasts over millions of data points, O(n²) isn't a trade-off; it's a dealbreaker. The monotonic stack pattern is one of those rare tools that turns a quadratic problem into a linear one without any exotic data structure magic — just a regular stack used with discipline.

The core problem it solves is deceptively simple: for each element in an array, find the nearest element to its left or right that is greater or smaller than it. Brute force checks every pair. The monotonic stack makes one observation that changes everything — if you're scanning left-to-right and you encounter an element that 'dominates' the element on top of the stack, the dominated element has found its answer. You process it, pop it, and never look at it again. That amortized O(1) pop-per-element is where the O(n) total time comes from.

By the end of this article you'll be able to implement both monotonically increasing and decreasing stacks from scratch, identify which variant a problem requires, handle tricky edge cases like duplicate values and circular arrays, and explain the amortized time complexity to an interviewer without flinching. You'll also walk away with a mental checklist that maps problem phrasing directly to the right stack orientation — so you stop guessing.

What is Monotonic Stack? — Plain English

A monotonic stack maintains elements in either strictly increasing or strictly decreasing order. When a new element is pushed, any elements on the stack that violate the monotonic property are popped first. The moment an element is popped because of a new incoming element, that defines a relationship — typically 'the next greater element' or 'the next smaller element'.

Monotonic stacks solve in O(n) problems that naively take O(n^2): next greater element, previous smaller element, largest rectangle in histogram, trapped rainwater.

This section sets the foundation. You'll see the pattern re‑occur in dozens of interview and production problems.

Mental Model
The Bouncer Analogy
A stack is like a bouncer at a club who keeps a list of people waiting. When a taller person arrives, the bouncer kicks out everyone shorter — those shorter people just found who's next taller.
  • The list (stack) always stays sorted by height (value).
  • When someone leaves (pop), they've found their answer.
  • If no one taller arrives, they stay in the list forever (answer = -1).
  • The bouncer never re‑adds a person — O(1) per event.
📊 Production Insight
Engineers often over‑engineer solutions when a monotonic stack works perfectly.
Use it before reaching for balanced BSTs or segment trees for 'nearest larger' queries.
It's cheaper in memory and faster in runtime.
🎯 Key Takeaway
A monotonic stack is a regular stack with a simple invariant: elements are always sorted.
That invariant turns O(n²) into O(n) for 'nearest larger/smaller' problems.

How Monotonic Stack Works — Step by Step

Let's build a monotonic decreasing stack to find the next greater element (NGE) for each index. The stack maintains indices of elements in decreasing order of their values (biggest on top? No — actually, decreasing stack means values decrease from bottom to top). Here's the algorithm:

  1. Initialize an empty stack.
  2. For each index i from 0 to n-1:
  3. - While stack is not empty AND arr[stack.top] < arr[i]:
  4. pop top index j → record result[j] = arr[i]
  5. - Push i onto stack.
  6. Any indices still in stack have no rightward greater elements → their result stays -1.

For an increasing stack (next smaller element), flip the condition to arr[stack.top] > arr[i].

Key insight: the popped element's answer is the current element. This is a one‑pass algorithm.

📊 Production Insight
The most common production bug: using < when you need <=.
For 'next greater or equal', change the condition.
Deduplicate by writing tests with arrays containing repeated values.
🎯 Key Takeaway
Decreasing stack → next greater. Increasing stack → next smaller.
Traverse right‑to‑left for 'previous' variants.
One condition sign change can flip all answers.
Choosing the Right Variant
IfNeed the next greater element (to the right)
UseUse monotonic decreasing stack (pop when stack top < current)
IfNeed the next smaller element (to the right)
UseUse monotonic increasing stack (pop when stack top > current)
IfNeed the previous greater element (to the left)
UseTraverse from right to left with decreasing stack
IfNeed the previous smaller element (to the left)
UseTraverse from right to left with increasing stack

Worked Example — Tracing the Algorithm

Find next greater element for [4, 5, 2, 10, 8].

i=0 (4): stack empty. Push 0. stack=[0]. i=1 (5): arr[0]=4 < 5. Pop 0. result[0]=5. Stack empty. Push 1. stack=[1]. i=2 (2): arr[1]=5 >= 2. Push 2. stack=[1,2]. i=3 (10): arr[2]=2 < 10. Pop 2. result[2]=10. arr[1]=5 < 10. Pop 1. result[1]=10. Stack empty. Push 3. stack=[3]. i=4 (8): arr[3]=10 >= 8. Push 4. stack=[3,4]. End: stack has [3,4] remaining. result[3]=-1, result[4]=-1.

Result: [5, 10, 10, -1, -1]

monotonic_stack_trace.py · PYTHON
123456789101112
def next_greater_element(arr):
    n = len(arr)
    result = [-1] * n
    stack  = []
    for i in range(n):
        while stack and arr[stack[-1]] < arr[i]:
            j = stack.pop()
            result[j] = arr[i]
        stack.append(i)
    return result

print(next_greater_element([4, 5, 2, 10, 8]))
▶ Output
[5, 10, 10, -1, -1]
📊 Production Insight
When debugging, print the stack after each iteration.
You'll often see stack=[index of 10, index of 8] at the end.
If you see unexpected pops, check the condition — likely using <= instead of <.
🎯 Key Takeaway
Trace by hand before coding. The single‑pass logic is subtle.
Each pop records an answer; remaining entries get -1.

Implementation — Python and Java

Below are clean implementations in Python and Java. The pattern is identical across languages: a stack of indices, a while loop with the monotonic condition, and a final sweep for unpopped indices.

Important: always store indices in the stack, not values. You need the index to fill the result array. The value is retrieved via arr[stack.top].

monotonic_stack_pattern.py · PYTHON
12345678910111213141516171819
def next_greater_element(arr):
    result = [-1] * len(arr)
    stack = []
    for i in range(len(arr)):
        while stack and arr[stack[-1]] < arr[i]:
            idx = stack.pop()
            result[idx] = arr[i]
        stack.append(i)
    return result

def next_smaller_element(arr):
    result = [-1] * len(arr)
    stack = []
    for i in range(len(arr)):
        while stack and arr[stack[-1]] > arr[i]:
            idx = stack.pop()
            result[idx] = arr[i]
        stack.append(i)
    return result
📊 Production Insight
In Java, use Deque<Integer> instead of Stack (deprecated).
ArrayDeque is faster and not synchronized.
Watch out for ArrayIndexOutOfBounds when accessing arr[stack.peek()] on empty stack.
🎯 Key Takeaway
Always store indices, not values.
Use Deque in Java, list in Python.
One while loop per element structure.

Real‑World Use Cases — Beyond the Basics

The monotonic stack isn't just an interview trick. It appears in production code for:

  • Largest Rectangle in Histogram: For each bar, the area is height * (right_smaller - left_smaller - 1). Monotonic increasing stack finds both boundaries in one pass.
  • Trapping Rain Water: Maintain stack of decreasing heights; when a taller bar arrives, compute water trapped above the popped bar.
  • Stock Span Problem: For each day, find how many consecutive days before today the price was ≤ today's. Monotonic decreasing stack with indices.

Each of these uses the same core pattern with a small twist.

🔥Histogram Trick
For largest rectangle, after the loop, process remaining stack as if a bar of height 0 appeared at index n. This avoids off‑by‑one errors.
📊 Production Insight
When implementing histogram, don't forget to process remaining stack.
A common bug is forgetting that the right boundary is 'n' for bars never popped.
Test with a decreasing sequence (e.g., [5,4,3,2,1]) — the max area should be 5.
🎯 Key Takeaway
Monotonic stack is the backbone of histogram, rainwater, and stock span.
Each adaptation changes only the condition and what you record when popping.

Complexity Analysis — Why O(n) Amortised

Each element is pushed onto the stack exactly once. Each element can be popped at most once. Total number of push/pop operations is therefore ≤ 2n. The inner while loop may run many times for a single outer iteration, but across the entire run, the total pops equal the number of pushes. This gives an amortised O(1) per element. Space is O(n) for the stack in the worst case (e.g., decreasing array for next greater).

Formally: each iteration of the while loop pops an element, and each element is popped at most once → O(n) total while iterations.

📊 Production Insight
Amortised analysis matters when your stack grows large (e.g., 10M elements).
Memory might spike to O(n) for sorted input – that's the worst case.
But time remains linear, which is usually the bottleneck.
🎯 Key Takeaway
Each element pushed once, popped at most once → O(n) total.
Space O(n) worst‑case. Use profiler before scaling if memory is tight.
🗂 Monotonic Stack vs Brute Force
Core differences for next greater element
AspectBrute Force (Nested Loop)Monotonic Stack
Time ComplexityO(n²)O(n)
Space ComplexityO(1) (excluding output)O(n) stack overhead
ImplementationSimple, two loopsSingle loop + while, harder to get right
ScalabilityFails beyond ~10⁴ elementsHandles 10⁶+ comfortably

🎯 Key Takeaways

  • A monotonic stack maintains sorted order, discarding elements that can never be useful.
  • Each element is pushed and popped at most once — total O(n) time for the entire algorithm.
  • Decreasing stack: pop when arr[new] > arr[top] → finds next greater element.
  • Increasing stack: pop when arr[new] < arr[top] → finds next smaller element.
  • Common applications: next greater element, previous smaller element, histogram rectangles, trapped rainwater.
  • Always use index stack, not value stack, to correctly fill result array.

⚠ Common Mistakes to Avoid

    Using the wrong comparison sign (<= vs <)
    Symptom

    Results are off by one, or equal values are not handled correctly.

    Fix

    Decide if you need strictly greater/smaller or non‑strict. For 'next greater', use < for strict, <= for greater‑or‑equal.

    Forgetting to process remaining elements in the stack at the end
    Symptom

    Uninitialised (or wrong) values for indices that never got popped.

    Fix

    After the loop, the default value (e.g., -1) already applies. But verify — if you used a different default, you must explicitly set them.

    Storing values instead of indices in the stack
    Symptom

    Cannot correctly fill the result array because you don't know the index of the popped element.

    Fix

    Always push i (the index), not arr[i]. Retrieve value via arr[stack.peek()] when comparing.

Interview Questions on This Topic

  • QWhat is a monotonic stack and when should you use one?JuniorReveal
    A monotonic stack maintains elements in increasing or decreasing order as they are processed. Use it for problems that ask for the 'next / previous greater / smaller element' for every index in an array. Typical examples: next greater element, largest rectangle in histogram, trapped rainwater, and stock span.
  • QWalk through the 'next greater element' algorithm with a concrete example.Mid-levelReveal
    Given array [4,5,2,10,8], we compute next greater element for each index: - i=0 (4): stack empty → push 0 - i=1 (5): stack top 0 has value 4 < 5 → pop 0, result[0]=5; push 1 - i=2 (2): stack top 1 has value 5 ≥ 2 → push 2 - i=3 (10): pop 2 (2<10) result[2]=10; pop 1 (5<10) result[1]=10; push 3 - i=4 (8): stack top 3 has value 10 ≥ 8 → push 4 End: remaining indices 3 and 4 have no greater element → result[3]=-1, result[4]=-1. Final: [5,10,10,-1,-1]
  • QHow does a monotonic stack achieve O(n) when a naive solution is O(n^2)?SeniorReveal
    Each element is pushed onto the stack exactly once and popped at most once. The total number of push/pop operations across the entire algorithm is O(n). Even though the inner while loop may run many times in one iteration, each pop is attributed to a unique element, so total operations are linear. This is amortised O(n) time, with O(n) space for the stack.
  • QWhat is the difference between a monotonically increasing and decreasing stack?JuniorReveal
    A monotonically increasing stack maintains elements in increasing order (bottom to top). It is used to find the next smaller element because when a smaller element arrives, it pops larger ones (which found their smaller element). A monotonically decreasing stack maintains decreasing order and finds next greater elements. The choice depends on which direction of comparison you need.

Frequently Asked Questions

What makes a stack 'monotonic' and why does that help?

A stack is monotonic if its elements are always in sorted order (increasing or decreasing) from bottom to top. This ensures that when an element is popped because a larger/smaller element arrived, the pop answers the 'next greater/smaller' question directly. Without the monotonic invariant, you'd need to scan the entire array for each element — O(n^2).

What is the classic 'largest rectangle in histogram' and how does the monotonic stack help?

For each bar, the largest rectangle with that bar as the shortest bar extends left to the first shorter bar and right to the first shorter bar. A monotonic increasing stack (of indices in increasing height order) finds both left and right boundaries: when a bar is popped because a shorter bar arrived, we know the right boundary is the current index and the left boundary is the new stack top.

Can a monotonic stack process elements from right to left?

Yes. For 'previous greater element', traverse right to left and use the same logic. The stack stores candidates that might be the previous greater element for future (earlier) indices. Right-to-left traversal is also used for next smaller element in some formulations.

What if the array is circular (e.g., next greater element II)?

For circular arrays, simulate the wrap-around by iterating twice (2n) while only first n indices receive results. Use modulo indexing. The stack may hold indices up to 2n-1, but ensure you only assign results for indices < n. This extension still runs in O(n) time and O(n) space.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousDeque — Double Ended QueueNext →Balanced Parentheses Problem
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged