Stack / Monotonic Stack

Medium
Time: O(n)Space: O(n)
0/20
problems solved
0%
1.

What is a Stack?

A stack is a linear data structure that follows the Last-In, First-Out (LIFO) principle. Think of it like a stack of plates in a cafeteria: you add new plates to the top, and when you need a plate, you take from the top. The last plate you added is the first one you remove.

Real-World Analogies:

  • Stack of plates: add/remove only from the top
  • Browser back button: most recent page is accessed first
  • Undo functionality: most recent action is undone first
  • Function call stack: most recent function returns first

The LIFO Principle: Whatever you put in last comes out first. This is fundamentally different from a queue (FIFO - First-In, First-Out) where items come out in the order they went in.

Add items: A, B, C
Stack: [A, B, C] (C on top)
Remove items:
Pop → C (most recent)
Pop → B
Pop → A
Items come out in REVERSE order!

This reversal property is extremely useful for many algorithmic problems.

2.

Core Stack Operations

A stack has a small set of fundamental operations, all running in O(1) constant time:

OperationDescriptionTime
push(x)Add element x to the top of the stackO(1)
pop()Remove and return the top elementO(1)
peek() / top()Return the top element without removing itO(1)
isEmpty()Check if the stack has any elementsO(1)
size()Return the number of elementsO(1)

Why O(1)? All operations only touch the top of the stack. We never need to search or shift elements.

Visual Example:

Operation Stack State Return
─────────────────────────────────────────
push(10) [10] -
push(20) [10, 20] -
push(30) [10, 20, 30] -
peek() [10, 20, 30] 30
pop() [10, 20] 30
pop() [10] 20
isEmpty() [10] false
pop() [] 10
isEmpty() [] true
Code
Java
3.

When to Use a Stack

Stacks are the right tool when your problem has one of these characteristics:

1. Matching Pairs When you need to match opening and closing elements:

  • Parentheses: (), [], {}
  • HTML/XML tags: <div>...</div>
  • Nested structures

2. Reverse Order Processing When the most recently added item must be processed first:

  • Undo operations
  • Browser history
  • Backtracking

3. Nested Structures When you encounter nested elements that must be processed inside-out:

  • Decode string: 3[a2[c]]
  • Calculator with parentheses
  • Recursive structures converted to iterative

4. "Next Greater/Smaller" Problems When finding the next element that satisfies a condition:

  • Daily temperatures (next warmer day)
  • Stock span problems
  • Largest rectangle in histogram

5. Expression Evaluation When evaluating mathematical expressions:

  • Infix to postfix conversion
  • Postfix (RPN) evaluation
  • Calculator problems

Recognition Signals in Problem Statements:

  • "Matching", "balanced", "valid parentheses"
  • "Next greater", "next smaller"
  • "Nested", "recursive structure"
  • "Evaluate expression"
  • "Most recent", "last added"
4.

Pattern 1: Matching Parentheses

The most classic stack problem: determine if brackets are properly matched and nested.

The Key Insight: Valid parentheses follow LIFO order: the most recently opened bracket must be the first one closed. If we see {[, the next closing bracket MUST be ] (to close [), not } (to close {).

Algorithm:

  1. For each character in the string:
    • If opening bracket (, [, {: push onto stack
    • If closing bracket ), ], }:
      • Check if stack is empty (invalid if so)
      • Pop and verify it matches the closing bracket
  2. At the end: stack must be empty (all brackets closed)

Walkthrough: "([{}])"

Char Action Stack After
──────────────────────────────────────────
( Opening → push ['(']
[ Opening → push ['(', '[']
{ Opening → push ['(', '[', '{']
} Closing → pop '{' ✓ ['(', '[']
] Closing → pop '[' ✓ ['(']
) Closing → pop '(' ✓ []
End Stack empty ✓ Valid!

Walkthrough: "([)]"

Char Action Stack After
──────────────────────────────────────────
( Opening → push ['(']
[ Opening → push ['(', '[']
) Closing → pop '[' ✗ INVALID!
Expected ')' to close '[', got mismatch

Edge Cases to Handle:

  • Empty string → valid
  • Only opening brackets "(((" → invalid (stack not empty)
  • Only closing brackets ")))" → invalid (pop from empty stack)
  • Single bracket → invalid
Code
Java
5.

Template: Matching Pairs

Here's a reusable template for all matching/balancing problems:

Template Structure:

1. Create empty stack
2. Create mapping of closing → opening
3. For each character:
a. If opening: push to stack
b. If closing:
- If stack empty or top doesn't match: INVALID
- Else: pop (matched successfully)
4. Return: stack is empty

Variations of This Pattern:

ProblemWhat Changes
Valid ParenthesesBasic template
Minimum RemoveTrack indices of invalid brackets
Min Insertions to BalanceCount what's needed to fix
Score of ParenthesesCompute value while matching
Longest Valid ParenthesesTrack lengths with DP

Extended Example: Track indices for removal

// Minimum Remove to Make Valid Parentheses
function minRemoveToMakeValid(s) {
const stack = []; // Store indices of '('
const toRemove = new Set();
for (let i = 0; i < s.length; i++) {
if (s[i] === '(') {
stack.push(i); // Track index
} else if (s[i] === ')') {
if (stack.length > 0) {
stack.pop(); // Matched!
} else {
toRemove.add(i); // Unmatched ')'
}
}
}
// Remaining '(' in stack are unmatched
for (const idx of stack) {
toRemove.add(idx);
}
// Build result without invalid indices
return [...s].filter((_, i) => !toRemove.has(i)).join('');
}
6.

Interactive: Valid Parentheses Visualizer

Watch how the stack grows with opening brackets and shrinks when matching closing brackets are found. Notice how the LIFO property ensures that inner brackets are always closed before outer ones.

Valid Parentheses

Stack for matching opening and closing brackets

Speed:
Input String:
(
[
{
}
]
)
Stack (top on right):
Empty
Matching Pairs:
( ↔ )[ ↔ ]{ ↔ }
Click Play to validate parentheses

Key Insight: Opening brackets push, closing brackets pop and check match. Stack must be empty at end for valid string.

7.

Pattern 2: Monotonic Stack Introduction

A monotonic stack is a stack that maintains elements in sorted order (either always increasing or always decreasing from bottom to top). This powerful technique solves "next greater/smaller element" problems in O(n) time.

The Core Idea: Before pushing a new element, we pop all elements that would violate the monotonic property. When an element gets popped, we've found something about it (like its "next greater" element).

Two Types:

1. Monotonic Decreasing Stack (elements decrease bottom→top)

  • Pop when: current element > top of stack
  • Use for: "Next Greater Element" problems
  • Stack keeps: elements waiting for something greater

2. Monotonic Increasing Stack (elements increase bottom→top)

  • Pop when: current element < top of stack
  • Use for: "Next Smaller Element" problems
  • Stack keeps: elements waiting for something smaller

Visual: Decreasing Stack

Push 5: [5] (bottom→top: 5)
Push 3: [5, 3] (5 > 3 ✓ decreasing)
Push 1: [5, 3, 1] (3 > 1 ✓ decreasing)
Push 4: Pop 1, pop 3 (4 > 1, 4 > 3 - violated!)
[5, 4] (5 > 4 ✓ restored)

When we popped 1 and 3, we learned that 4 is their "next greater element"!

Why O(n) Time? Each element is pushed exactly once and popped at most once. Even though there's a while loop inside the for loop, total operations across all iterations is at most 2n = O(n).

8.

Pattern 2a: Next Greater Element

Problem: For each element in an array, find the next element to the right that is greater. If none exists, use -1.

Example: [2, 1, 2, 4, 3][4, 2, 4, -1, -1]

Why Monotonic Decreasing Stack? We keep elements "waiting" for a greater element. When a greater element arrives, all smaller elements on the stack have found their answer.

Algorithm:

  1. Initialize result array with -1 (default: no greater element)
  2. Use stack to store INDICES (not values)
  3. For each element:
    • While stack not empty AND current > stack top's value:
      • Pop index, set result[popped] = current
    • Push current index
  4. Elements remaining in stack have no next greater

Detailed Walkthrough: [2, 1, 2, 4, 3]

i=0, val=2: Stack empty, push index 0
Stack: [0] (values: [2])
i=1, val=1: 1 < 2, just push index 1
Stack: [0, 1] (values: [2, 1])
i=2, val=2: 2 > 1, pop index 1 → result[1] = 2
2 = 2, stop popping, push index 2
Stack: [0, 2] (values: [2, 2])
Result: [-1, 2, -1, -1, -1]
i=3, val=4: 4 > 2, pop index 2 → result[2] = 4
4 > 2, pop index 0 → result[0] = 4
Stack empty, push index 3
Stack: [3] (values: [4])
Result: [4, 2, 4, -1, -1]
i=4, val=3: 3 < 4, just push index 4
Stack: [3, 4] (values: [4, 3])
Result: [4, 2, 4, -1, -1] ✓ Final!

Key Insight: Store INDICES not values. This lets us:

  1. Update the result array at the correct position
  2. Calculate distances when needed (like Daily Temperatures)
Code
Java
9.

Interactive: Next Greater Element Visualizer

See how elements wait on the monotonic decreasing stack until a larger element arrives. When a larger element comes, it "resolves" all smaller elements by becoming their next greater element.

Next Greater Element

Monotonic decreasing stack - pop when current greater than top

Speed:
Input Array:
2[0]
1[1]
2[2]
4[3]
3[4]
Monotonic Decreasing Stack (indices):
Empty
Result (Next Greater):
Click Play to find next greater elements

Key Insight: Elements wait on stack until a larger element "answers" them. Each element pushed once, popped at most once = O(n).

10.

Pattern 2b: Daily Temperatures

Problem: Given daily temperatures, find how many days until a warmer day.

[73, 74, 75, 71, 69, 72, 76, 73][1, 1, 4, 2, 1, 1, 0, 0]

This is Next Greater Element, but we return distance instead of value.

The Only Difference:

// Next Greater Element:
result[idx] = nums[i]; // The greater VALUE
// Daily Temperatures:
result[idx] = i - idx; // The DISTANCE to greater

Walkthrough:

Day 0 (73°): Stack empty, push 0
Stack: [0]
Day 1 (74°): 74 > 73, pop 0 → result[0] = 1-0 = 1
Push 1. Stack: [1]
Day 2 (75°): 75 > 74, pop 1 → result[1] = 2-1 = 1
Push 2. Stack: [2]
Day 3 (71°): 71 < 75, push 3
Stack: [2, 3]
Day 4 (69°): 69 < 71, push 4
Stack: [2, 3, 4]
Day 5 (72°): 72 > 69, pop 4 → result[4] = 5-4 = 1
72 > 71, pop 3 → result[3] = 5-3 = 2
72 < 75, push 5
Stack: [2, 5]
Day 6 (76°): 76 > 72, pop 5 → result[5] = 6-5 = 1
76 > 75, pop 2 → result[2] = 6-2 = 4
Stack empty, push 6
Stack: [6]
Day 7 (73°): 73 < 76, push 7
Stack: [6, 7]
Result: [1, 1, 4, 2, 1, 1, 0, 0]
Days 6 and 7 have no warmer day (remain 0)
Code
Java
11.

Pattern 2c: Next Smaller Element (Increasing Stack)

For "next smaller" problems, we flip the stack type:

Monotonic Increasing Stack:

  • Elements increase from bottom to top
  • Pop when: current < top of stack
  • Popped element has found its "next smaller"

Example: Find next smaller element for [4, 2, 5, 3, 1]

i=0, val=4: Stack empty, push 0
Stack: [0] (vals: [4])
i=1, val=2: 2 < 4, pop 0 → nextSmaller[0] = 2
Push 1. Stack: [1] (vals: [2])
i=2, val=5: 5 > 2, push 2
Stack: [1, 2] (vals: [2, 5])
i=3, val=3: 3 < 5, pop 2 → nextSmaller[2] = 3
3 > 2, push 3
Stack: [1, 3] (vals: [2, 3])
i=4, val=1: 1 < 3, pop 3 → nextSmaller[3] = 1
1 < 2, pop 1 → nextSmaller[1] = 1
Push 4. Stack: [4] (vals: [1])
Result: [2, 1, 3, 1, -1]

The Pattern Flip:

FindingStack TypePop Condition
Next GreaterDecreasingcurrent > top
Next SmallerIncreasingcurrent < top
Previous GreaterDecreasingIterate right→left
Previous SmallerIncreasingIterate right→left
Code
Java
12.

Pattern 2d: Largest Rectangle in Histogram

Problem: Find the largest rectangular area in a histogram.

Key Insight: For each bar, find how far left and right it can extend (until a shorter bar stops it). Then: area = height × width.

This is a "next smaller element" problem! A bar stops extending when it hits a shorter bar.

Algorithm:

  1. Use monotonic INCREASING stack (pop when current < top)
  2. When we pop a bar, we can calculate its area:
    • Height = the popped bar's height
    • Left boundary = new stack top (or start if empty)
    • Right boundary = current position
    • Width = right - left - 1
  3. Add a sentinel height 0 at the end to flush all bars

Walkthrough: [2, 1, 5, 6, 2, 3]

i=0, h=2: Push 0. Stack: [0]
i=1, h=1: 1 < 2, pop 0
Height=2, width=1, area=2
Push 1. Stack: [1]
i=2, h=5: 5 > 1, push 2. Stack: [1, 2]
i=3, h=6: 6 > 5, push 3. Stack: [1, 2, 3]
i=4, h=2: 2 < 6, pop 3
Height=6, width=1, area=6
2 < 5, pop 2
Height=5, width=2, area=10 ★ MAX!
2 > 1, push 4. Stack: [1, 4]
i=5, h=3: 3 > 2, push 5. Stack: [1, 4, 5]
i=6, h=0: (sentinel) Pop all remaining
Pop 5: height=3, width=1, area=3
Pop 4: height=2, width=4, area=8
Pop 1: height=1, width=6, area=6
Maximum Area = 10

Why Width = i - stack.top - 1? The bar can extend from (stack.top + 1) to (i - 1), giving width = i - stack.top - 1. If stack is empty, the bar extends from 0 to i-1, giving width = i.

Code
Java
13.

Interactive: Largest Rectangle Visualizer

Watch how the monotonic increasing stack tracks bar boundaries. When a shorter bar arrives, taller bars get popped and their maximum possible rectangles are calculated.

Largest Rectangle in Histogram

Monotonic increasing stack - find max area by tracking boundaries

Speed:
Histogram:
2
[0]
1
[1]
5
[2]
6
[3]
2
[4]
3
[5]
Monotonic Increasing Stack:
Empty
Current Position
-
Max Area
0
Click Play to find largest rectangle in histogram

Key Insight: When popping bar i, it can extend from stack top to current position. Add sentinel 0 at end to flush all remaining bars.

14.

Pattern 3: Expression Evaluation

Stacks excel at evaluating mathematical expressions because of operator precedence and nested parentheses.

The Challenge: In 3 + 2 * 4, we can't just compute left-to-right because * has higher precedence than +. We need to compute 2 * 4 first.

Stack Solution for Basic Calculator II: Process operators based on precedence:

  • * and /: High precedence → evaluate immediately
  • + and -: Low precedence → defer (push to stack)

Algorithm:

  1. Track current number being built
  2. Track previous operator (starts as +)
  3. When we see a new operator or reach the end:
    • Apply the previous operator to current number
    • +: push number
    • -: push -number
    • *: pop, multiply, push result
    • /: pop, divide, push result
  4. Sum all values in stack

Walkthrough: "3+2*4-5"

Char '3': num = 3
Char '+': prevOp='+', push 3. Stack: [3]
num = 0, prevOp = '+'
Char '2': num = 2
Char '*': prevOp='+', push 2. Stack: [3, 2]
num = 0, prevOp = '*'
Char '4': num = 4
Char '-': prevOp='*', pop 2, push 2*4=8. Stack: [3, 8]
num = 0, prevOp = '-'
Char '5': num = 5
End: prevOp='-', push -5. Stack: [3, 8, -5]
Result: 3 + 8 + (-5) = 6 ✓

Why Push -number for Subtraction? By converting a - b to a + (-b), we can sum the entire stack at the end without tracking signs separately.

Code
Java
15.

Pattern 4: Nested Structures (Decode String)

When dealing with nested structures like 3[a2[c]], stacks help us save and restore state as we enter and exit nested levels.

Problem: Decode 3[a2[c]]accaccacc

The Key Insight: When we see [, we're about to enter a nested level. We need to:

  1. Save current state (string so far, repeat count)
  2. Start fresh for the inner expression

When we see ], we:

  1. Restore outer state
  2. Combine: outer string + (inner string × count)

Algorithm:

  1. Maintain: current string, current number
  2. On [:
    • Push (current string, current number) to stacks
    • Reset both for inner expression
  3. On ]:
    • Pop count and previous string
    • current = previous + current.repeat(count)
  4. On digit: build number
  5. On letter: append to current string

Walkthrough: "3[a2[c]]"

Step Char Action strStack countStack curr num
──────────────────────────────────────────────────────────────────────────
1 '3' Build number [] [] "" 3
2 '[' Push("", 3), reset [""] [3] "" 0
3 'a' Append to curr [""] [3] "a" 0
4 '2' Build number [""] [3] "a" 2
5 '[' Push("a", 2), reset ["","a"] [3,2] "" 0
6 'c' Append to curr ["","a"] [3,2] "c" 0
7 ']' Pop: prev="a", cnt=2 [""] [3] "acc" 0
curr = "a" + "c"×2
8 ']' Pop: prev="", cnt=3 [] [] "accaccacc"
curr = "" + "acc"×3
Result: "accaccacc"
Code
Java
16.

Related: Queues and Deques

Understanding related data structures helps you choose the right tool:

Queue (FIFO - First In, First Out)

  • Add to back, remove from front
  • Like a line at a store
  • Use for: BFS, level-order traversal, task scheduling

Deque (Double-Ended Queue)

  • Add/remove from both ends
  • Combines stack + queue capabilities
  • Use for: Sliding window maximum, palindrome checking

Comparison:

Structure Add Remove Use Case
─────────────────────────────────────────────────
Stack Top only Top only LIFO: undo, matching
Queue Back only Front only FIFO: BFS, scheduling
Deque Both ends Both ends Flexible: sliding window

Implement Queue with Two Stacks: A classic interview problem! Use one stack for enqueue, one for dequeue.

class MyQueue {
constructor() {
this.inStack = []; // For push
this.outStack = []; // For pop/peek
}
push(x) {
this.inStack.push(x);
}
pop() {
this._transfer();
return this.outStack.pop();
}
_transfer() {
if (this.outStack.length === 0) {
while (this.inStack.length > 0) {
this.outStack.push(this.inStack.pop());
}
}
}
}

Amortized O(1): Each element is moved at most once from inStack to outStack.

17.

Problem Recognition Guide

Quick Decision Tree:

What does the problem involve?
├─ Matching pairs (brackets, tags)?
│ └─ Basic Stack: push open, pop on close
├─ "Next greater" for each element?
│ └─ Monotonic DECREASING stack
│ Pop when: current > top
├─ "Next smaller" for each element?
│ └─ Monotonic INCREASING stack
│ Pop when: current < top
├─ Expression with +, -, *, /?
│ └─ Stack for values, handle precedence
│ * / immediately, + - deferred
├─ Nested structures [a[b]]?
│ └─ Two stacks: save/restore state
│ Push on '[', pop on ']'
├─ Sliding window max/min?
│ └─ Monotonic DEQUE
│ Remove from front when out of window
└─ Undo/redo, backtracking?
└─ Basic stack: push actions, pop to undo

Common Problem Patterns:

Problem TypeStack TypeKey Insight
Valid ParenthesesBasicMatch on close
Daily TemperaturesMono DecreasingStore indices, return distance
Next Greater ElementMono DecreasingStore indices, return value
Largest RectangleMono IncreasingCalculate area when popping
Basic CalculatorValue stackPrecedence: */ before +-
Decode StringTwo stacksSave context on '['
Min StackAux stackTrack min at each level
Asteroid CollisionSimulationPop when collision occurs
18.

Common Mistakes and Tips

Mistake 1: Wrong Comparison Direction

// WRONG: This is for next SMALLER
while (stack.length && nums[stack.at(-1)] > nums[i])
// CORRECT: For next GREATER, pop when top is SMALLER
while (stack.length && nums[stack.at(-1)] < nums[i])

Mistake 2: Forgetting Remaining Elements

// After the loop, elements still in stack have no answer
// Make sure you initialize result array correctly!
const result = new Array(n).fill(-1); // Default for "no answer"

Mistake 3: Storing Values Instead of Indices

// WRONG: Loses position information
stack.push(nums[i]);
// CORRECT: Store indices, look up values
stack.push(i);
const value = nums[stack[stack.length - 1]];

Mistake 4: Empty Stack Check

// WRONG: Will crash on empty stack
const top = stack[stack.length - 1];
// CORRECT: Always check before accessing
if (stack.length > 0) {
const top = stack[stack.length - 1];
}

Mistake 5: Integer Division in Calculator

// WRONG: JavaScript division gives float
stack.push(stack.pop() / num);
// CORRECT: Truncate toward zero
stack.push(Math.trunc(stack.pop() / num));

Pro Tips:

  1. Sentinel values: Add dummy values to avoid edge cases (histogram adds 0 at end)
  2. Circular arrays: Iterate 2n times using i % n
  3. Debug: Print stack state at each step when confused
  4. Two stacks: When you need to track two things (count + string)
19.

Complexity Analysis

Time Complexity: O(n) for Monotonic Stack

Despite the nested while loop, monotonic stack is O(n) because:

  • Each element is pushed exactly ONCE
  • Each element is popped at most ONCE
  • Total operations across all iterations ≤ 2n
Pushes: n elements × 1 push each = n
Pops: n elements × at most 1 pop each = n
Total: 2n = O(n)

Space Complexity: O(n)

Worst case occurs with monotonically sorted input:

  • Decreasing stack + increasing input: all elements stay
  • Increasing stack + decreasing input: all elements stay

Operation Costs:

OperationTimeNotes
pushO(1)Append to end
popO(1)Remove from end
peekO(1)Access last element
isEmptyO(1)Check length

Language Notes:

  • JavaScript: Arrays work well; use .push(), .pop(), .at(-1)
  • Java: Use ArrayDeque, not legacy Stack class
  • Python: Lists work; use .append(), .pop(), [-1]
  • C++: Use std::stack or std::vector

When O(n) Isn't Enough: For problems requiring both next greater AND previous greater, you might run two passes or use a more complex approach, but each pass is still O(n).

20.

Advanced: Circular Arrays

For circular arrays (like Next Greater Element II), the last element wraps around to the first.

Problem: [1, 2, 1][2, -1, 2] (the last 1 sees the first 2)

Solution: Iterate 2n times, using i % n for the actual index.

function nextGreaterCircular(nums) {
const n = nums.length;
const result = new Array(n).fill(-1);
const stack = [];
// Iterate twice through the array
for (let i = 0; i < 2 * n; i++) {
const idx = i % n; // Actual index in array
while (stack.length > 0 &&
nums[stack[stack.length - 1]] < nums[idx]) {
result[stack.pop()] = nums[idx];
}
// Only push during first pass
// (second pass is just for finding answers)
if (i < n) {
stack.push(i);
}
}
return result;
}

Walkthrough: [1, 2, 1]

First pass (i = 0, 1, 2):
i=0: push 0. Stack: [0]
i=1: 2>1, pop 0→result[0]=2. Push 1. Stack: [1]
i=2: 1<2, push 2. Stack: [1, 2]
Second pass (i = 3, 4, 5 → idx = 0, 1, 2):
i=3 (idx=0): 1<2, 1<1... nothing
i=4 (idx=1): 2>1, pop 2→result[2]=2. Stack: [1]
i=5 (idx=2): 1<2, nothing
Result: [2, -1, 2] ✓

Test Your Knowledge

Take a quick 15question quiz to test what you've learned.