Module 4.1

Stacks & Applications

Think of a stack like a pile of plates - you can only add or remove from the top! This simple yet powerful Last-In-First-Out (LIFO) principle is behind everything from your browser's back button to how your computer manages function calls. Let's master this essential data structure together!

40 min read
Intermediate
Interview Essential
What You'll Learn
  • LIFO principle
  • Stack implementation
  • Balanced parentheses
  • Expression evaluation
  • Monotonic stack pattern
Contents
01

Introduction to Stacks

Before diving into code, let's understand stacks through everyday examples. Once you see how natural this concept is, the implementation becomes much easier to grasp!

Stack (LIFO)

A Last-In-First-Out data structure where elements are added and removed from the same end (the "top"). Think of a stack of plates - you can only add or remove from the top.

Real-World Examples of Stacks

Stacks are everywhere in daily life! Understanding these examples will help you think in terms of LIFO:

Stack of Plates

At a buffet, plates are stacked. You take from the top (most recently added). You can't grab one from the middle without disaster!

Undo Button

Every action you do is "pushed" to history. Pressing Undo "pops" the last action. You can't undo the 5th action directly!

Browser Back Button

Pages you visit are stacked. Back button "pops" the current page to show the previous one. Forward re-pushes!

Stack of Books

Books piled on desk - you read the top one first. Adding a book goes on top. Taking one is always from the top!

Function Calls

When main() calls foo() which calls bar(), they stack up. bar() must finish first, then foo(), then main().

PEZ Dispenser

Candies are loaded from bottom, but dispensed from top. The last candy you put in comes out first!

Key Mental Model: Think of a stack as a "vertical" container where you can only access the top. You can't peek at or remove items from the middle or bottom without first removing everything above them. This restriction is what makes stacks simple and fast!

The Four Core Operations

Every stack supports exactly four operations. That's it! This simplicity is what makes stacks so powerful and efficient:

Push

Add element to top. O(1) time complexity.

Pop

Remove and return top element. O(1) time.

Peek

View top without removing. O(1) time.

isEmpty

Check if stack is empty. O(1) time.

Operation Time Description
push(x) O(1) Add x to top of stack
pop() O(1) Remove and return top element
peek()/top() O(1) Return top without removing
isEmpty() O(1) Check if stack is empty
size() O(1) Return number of elements
Visual: Stack Operations Step-by-Step
STARTING POINT
  Empty Stack:

     ┌─────┐
     │     │ ← top = -1 (empty)
     └─────┘

  stack.isEmpty()true
  stack.size()0
PUSH(10), PUSH(20), PUSH(30)
push(10):        push(20):        push(30):

   ┌─────┐         ┌─────┐         ┌─────┐
   │     │         │     │         │ 30  │ ← top
   ├─────┤         ├─────┤         ├─────┤
   │     │         │ 20  │ ← top   │ 20  │
   ├─────┤         ├─────┤         ├─────┤
   │ 10  │ ← top   │ 10  │         │ 10  │
   └─────┘         └─────┘         └─────┘

Each push adds to TOP, moves top pointer up!
POP() → returns 30
Before pop():           After pop():

     ┌─────┐               ┌─────┐
     │ 30  │ → removed!    │     │
     ├─────┤               ├─────┤
     │ 20  │               │ 20  │ ← new top
     ├─────┤               ├─────┤
     │ 10  │               │ 10  │
     └─────┘               └─────┘

Pop returns 30, top now points to 20
PEEK() → returns 20
peek() - just LOOK, don't touch:

     ┌─────┐
     │ 20  │ ← peek returns 20
     ├─────┤      (still there!)10  │
     └─────┘

Stack unchanged after peek!
size() still returns 2
EDGE CASES
Stack Underflow:
  Calling pop() or peek() on empty stackThrows Exception!Always check isEmpty() first!

Stack Overflow (fixed size only):
  Calling push() when stack is fullThrows Exception!Check isFull() before push!
Key Insight: All operations happen at the TOP only! This is why they're all O(1) - we never need to traverse the stack.

Practice Questions: Stack Basics

Given operations:

push(5), push(3), pop(), push(7), push(2), pop(), peek()

Task: What value does peek() return? What elements remain in the stack (bottom to top)?

Show Solution
// Step by step:
// push(5) → Stack: [5]
// push(3) → Stack: [5, 3]
// pop()   → Returns 3, Stack: [5]
// push(7) → Stack: [5, 7]
// push(2) → Stack: [5, 7, 2]
// pop()   → Returns 2, Stack: [5, 7]
// peek()  → Returns 7, Stack: [5, 7]

// Answer: peek() returns 7
// Remaining stack (bottom to top): [5, 7]

Task: Explain LIFO with a real-world analogy. Why can't we access the middle element directly?

Show Solution
LIFO = Last In, First Out

Real-world examples:
1. Stack of plates - you take from the top
2. Browser back button - most recent page first
3. Undo in text editor - last action undone first
4. Call stack - most recent function returns first

Why no middle access?
- Stack's design only exposes the top
- To reach middle, you'd pop everything above
- This constraint enables O(1) operations
- If you need middle access, use an array/list instead

Task: Explain what "Stack Underflow" means and how to prevent it.

Show Solution
// Stack Underflow = trying to pop/peek empty stack
// In Java: throws EmptyStackException or NoSuchElementException

// Prevention pattern:
if (!stack.isEmpty()) {
    int value = stack.pop();  // Safe!
}

// Or with ternary:
int top = stack.isEmpty() ? -1 : stack.peek();

// Best practice: ALWAYS check isEmpty() before pop/peek!
02

Stack Implementation

Now let's implement a stack from scratch! We'll build two versions: one using an array (fixed size) and one using a linked list (dynamic size). Understanding both will deepen your knowledge of data structures.

Method 1: Using Array

The simplest approach - use an array and a top variable to track the current position. Think of top as a finger pointing to the topmost element.

Why start top = -1? When the stack is empty, there's no valid element to point to. Using -1 means "nothing here yet". When we push the first element, ++top makes it 0, which is the first valid array index!

We need three things to build our stack: an array to hold the data, a top variable to track where the newest element is, and a capacity to know the maximum size. The constructor sets up these pieces. We start with top = -1 because the stack is empty - there's no valid element yet!

class ArrayStack {
    private int[] arr;      // Array to store elements
    private int top;        // Index of top element
    private int capacity;   // Maximum size
    
    public ArrayStack(int capacity) {
        this.capacity = capacity;
        this.arr = new int[capacity];
        this.top = -1;  // Empty stack starts at -1
    }
}

The push method adds a new element to the top of the stack. First, we check if there's room - if the array is already full, we can't add more! Then we use a clever trick: ++top increments the index first, then we store the value at that new position. This way, we move to the next empty slot and fill it in one step.

public void push(int x) {
    if (isFull()) {
        throw new RuntimeException("Stack Overflow");
    }
    arr[++top] = x;  // Increment top, then store
}
// Example: push(10) when top=-1 → top becomes 0, arr[0]=10

The pop method removes and returns the top element. Safety first - we check if the stack is empty before trying to remove anything! The expression arr[top--] does two things: it grabs the value at the current top position, then decrements the index. The element isn't really "deleted" - it's still in the array - but since top moved down, it's now considered "out of bounds" and will be overwritten on the next push.

public int pop() {
    if (isEmpty()) {
        throw new RuntimeException("Stack Underflow");
    }
    return arr[top--];  // Return value, then decrement top
}
// Example: pop() when top=2 → returns arr[2], top becomes 1

Sometimes you want to see what's on top without actually removing it - that's what peek does. Think of it like peeking at the top card of a deck without taking it. We simply return the value at the current top index, and the stack stays exactly the same. Don't forget to check if empty first - peeking at an empty stack would give us garbage data!

public int peek() {
    if (isEmpty()) {
        throw new RuntimeException("Stack is empty");
    }
    return arr[top];  // Just look, don't modify top
}
// Example: peek() when top=2 → returns arr[2], top stays 2

These helper methods tell us about the stack's current state. isEmpty() checks if top is -1, meaning nothing has been pushed yet. isFull() checks if we've used all available slots - since arrays are 0-indexed, the last valid index is capacity - 1. The size() method returns how many elements are currently in the stack, which is top + 1 because top is an index, not a count.

public boolean isEmpty() {
    return top == -1;  // No elements
}

public boolean isFull() {
    return top == capacity - 1;  // Array is full
}

public int size() {
    return top + 1;  // Number of elements (0-indexed)
}
Quick Summary:
  • ++top in push: Increment before using (pre-increment)
  • top-- in pop: Use then decrement (post-decrement)
  • Always check isEmpty() before pop/peek to avoid errors
  • Always check isFull() before push to prevent overflow

Method 2: Using Linked List

With a linked list, we get a dynamic-sized stack - no overflow possible (unless you run out of memory)! New elements are added at the head of the list, making push/pop both O(1).

Why add at head (not tail)? Adding at the head is O(1) - we just update the head pointer. Adding at the tail requires traversing the entire list to find the end - that's O(n)! So for a stack, the "top" is the head of the linked list.

In a linked list stack, each element lives in its own "node" object. A node holds two things: the actual value and a reference (pointer) to the next node below it. The top variable always points to the head node - the most recently added element. Unlike arrays, we don't need to declare a fixed size upfront. The stack can grow and shrink dynamically as we push and pop!

class LinkedListStack {
    private class Node {
        int val;        // Data stored in node
        Node next;      // Pointer to next node
        Node(int val) { this.val = val; }
    }
    
    private Node top;   // Points to top element (head)
    private int size;   // Track number of elements
}

To push onto a linked list stack, we create a brand new node with our value. Then we link this new node to whatever was previously on top (even if it's null for an empty stack). Finally, we update top to point to our new node. It's like adding a new link at the beginning of a chain - the new link becomes the first one, and it holds onto the rest of the chain.

public void push(int x) {
    Node newNode = new Node(x);  // Create new node
    newNode.next = top;          // Point to current top
    top = newNode;               // New node becomes top
    size++;
}
// Visual: [new] -> [old top] -> [rest...]

Popping from a linked list is like removing the first link of a chain. First, we save the value stored in the top node (we need to return it!). Then we simply move the top pointer to the next node in line. The old top node gets disconnected and Java's garbage collector will clean it up automatically. No shifting elements around like in an array!

public int pop() {
    if (isEmpty()) {
        throw new RuntimeException("Stack Underflow");
    }
    int val = top.val;    // Save value to return
    top = top.next;       // Move top to next node
    size--;
    return val;
}
// Visual: [removed] | [new top] -> [rest...]

The peek method lets us see the top value without modifying the stack - we just return top.val directly. For isEmpty(), we check if top is null, which means no nodes exist yet. Unlike the array version, there's no isFull() method because a linked list can keep growing until your computer runs out of memory. The size() method returns our manually tracked count.

public int peek() {
    if (isEmpty()) {
        throw new RuntimeException("Stack is empty");
    }
    return top.val;  // Just return, don't modify
}

public boolean isEmpty() {
    return top == null;  // No nodes exist
}

public int size() {
    return size;
}
Quick Summary:
  • Push: New node points to old top, becomes new top
  • Pop: Save value, move top pointer forward
  • No overflow: Can keep adding until memory runs out
  • Trade-off: Each node uses extra memory for the pointer

Array vs Linked List: Which to Choose?

Aspect Array Stack Linked List Stack
Size Fixed (can overflow) Dynamic (grows as needed)
Memory Efficient (just values) Extra overhead (stores pointers)
Cache Good (contiguous memory) Poor (scattered nodes)
When to Use Known max size, performance critical Unknown size, frequent resize needed
In Practice: Java's ArrayDeque uses a resizable array internally - best of both worlds! It starts small and grows when needed, while maintaining good cache performance.
Interactive: Stack Operations Simulator

Try pushing and popping values to see how a stack works! The stack has a maximum capacity of 5 elements.

Stack (capacity: 5)
Empty Stack
top = -1

Practice Questions: Implementation

Task: Compare Array-based and Linked List-based stack implementations. When would you prefer one over the other?

Show Solution
Array-based Stack:
✓ Better cache locality (contiguous memory)
✓ Less memory overhead (no node pointers)
✓ Faster in practice for most cases
✗ Fixed size (or costly resize)

Linked List Stack:
✓ Dynamic size - grows as needed
✓ No wasted memory from pre-allocation
✗ Extra memory for node pointers
✗ Worse cache performance

Use Array when:
- You know approximate max size
- Memory efficiency matters
- Performance is critical

Use Linked List when:
- Size varies wildly
- Can't predict maximum size
- Frequent resize would be costly

Task: Design a stack that supports push, pop, top, and getMin operations, all in O(1) time.

Hint: What if you kept track of the minimum at each level?

Show Solution
class MinStack {
    Deque stack = new ArrayDeque<>();
    Deque minStack = new ArrayDeque<>();
    
    void push(int val) {
        stack.push(val);
        // Push to minStack if it's new minimum or equal
        if (minStack.isEmpty() || val <= minStack.peek()) {
            minStack.push(val);
        }
    }
    
    void pop() {
        // If popped value was the min, pop from minStack too
        if (stack.pop().equals(minStack.peek())) {
            minStack.pop();
        }
    }
    
    int top() { return stack.peek(); }
    int getMin() { return minStack.peek(); }
}

Task: If an array-based stack doubles its capacity when full, what's the amortized time complexity of push?

Show Solution
When array is full and we push:
- Create new array of 2x size: O(n)
- Copy all elements: O(n)
- Add new element: O(1)
- Total for this push: O(n)

BUT amortized analysis:
- Resize happens rarely (after n pushes)
- n cheap O(1) pushes + 1 expensive O(n) push
- Total cost for n+1 operations: O(n)
- Amortized per operation: O(n)/(n+1) = O(1)

Answer: Amortized O(1) time complexity!

This is why ArrayList/ArrayDeque are efficient
despite occasional expensive resizes.
03

Java Stack & Deque

Good news - you don't have to build a stack from scratch every time! Java provides ready-to-use stack implementations. Let's explore the different options and learn which one to use in different situations.

First, import the classes you need. Java has a dedicated Stack class, but modern Java developers prefer Deque (Double-Ended Queue) which can work as both a stack and a queue. ArrayDeque is the most efficient implementation for stack operations.

import java.util.Stack;
import java.util.Deque;
import java.util.ArrayDeque;
import java.util.LinkedList;

The Stack class has been in Java since version 1.0. It works fine but has a hidden cost: it's synchronized, meaning it's thread-safe but slower for single-threaded programs (which is most of the time!). The methods are intuitive: push() adds, pop() removes, peek() looks, and isEmpty() checks.

// Using Stack class (older, synchronized)
Stack stack1 = new Stack<>();
stack1.push(1);
stack1.push(2);
int top = stack1.peek();   // 2
int popped = stack1.pop(); // 2
boolean empty = stack1.isEmpty();

This is the recommended approach in modern Java! ArrayDeque is faster than Stack because it's not synchronized. We declare it as type Deque (the interface) but create an ArrayDeque (the implementation). This is good practice - code to the interface! The methods work exactly the same way.

// Using Deque (preferred - faster, not synchronized)
Deque stack2 = new ArrayDeque<>();
stack2.push(1);            // Adds to front
stack2.push(2);
int top2 = stack2.peek();  // 2
int popped2 = stack2.pop(); // 2

You can also use LinkedList as a stack since it implements the Deque interface too. It's slightly slower than ArrayDeque for pure stack operations, but useful if you need both stack and queue functionality, or if you're doing lots of insertions in the middle.

// LinkedList can also be used as stack
Deque stack3 = new LinkedList<>();
stack3.push(1);
stack3.push(2);

Here's a pattern you'll use constantly in interviews and real code: always check if the stack is empty before calling pop() or peek(). If you try to pop from an empty stack, Java throws an exception and your program crashes! This defensive check prevents that.

// Common pattern: checking before pop
if (!stack.isEmpty()) {
    int val = stack.pop();
}
// Or use a ternary for peek
int topVal = stack.isEmpty() ? -1 : stack.peek();
When to Use What:
  • ArrayDeque: (Preferred) - faster, resizable, not synchronized
  • Stack class: (Legacy) - synchronized (thread-safe but slower)
  • LinkedList: Good if you need both stack & queue operations
  • Always check isEmpty(): Before pop() to avoid EmptyStackException!
Best Practice: Use Deque<E> interface with ArrayDeque<E> implementation instead of the legacy Stack class. It's faster because it's not synchronized.

Practice Questions: Java Stack & Deque

Task: Explain why Java's Stack class is not recommended. What should you use instead?

Show Solution
Why Stack is legacy:
1. Extends Vector (synchronized = slow)
2. Allows non-stack operations (get by index)
3. Thread-safety overhead when not needed
4. Older design decisions

Use instead:
  Deque stack = new ArrayDeque<>();

Why ArrayDeque is better:
✓ Not synchronized (faster for single-thread)
✓ Pure stack interface (no index access)
✓ Resizable array implementation
✓ Consistent with modern Java collections

Only use Stack if you NEED thread-safety
(and even then, consider explicit synchronization)

Task: When using Deque as a stack, you can use push/pop or addFirst/removeFirst. What's the difference?

Show Solution
// Functionally equivalent for stack use:
Deque stack = new ArrayDeque<>();

// Method 1: Stack-like methods
stack.push(1);     // Adds to front
stack.pop();       // Removes from front
stack.peek();      // Views front

// Method 2: Deque methods  
stack.addFirst(1);     // Same as push
stack.removeFirst();   // Same as pop
stack.peekFirst();     // Same as peek

// Key difference:
// push/pop work at the FRONT (index 0)
// This makes Deque act like a stack
// Both are O(1) operations

// Tip: Use push/pop for clarity
// It signals "I'm using this as a stack"

Task: Write code that safely pops from a stack, returning -1 if the stack is empty.

Show Solution
Deque stack = new ArrayDeque<>();

// Method 1: If-else check
int value;
if (!stack.isEmpty()) {
    value = stack.pop();
} else {
    value = -1;
}

// Method 2: Ternary operator (cleaner)
int value = stack.isEmpty() ? -1 : stack.pop();

// Method 3: pollFirst (returns null if empty)
Integer value = stack.pollFirst();
int result = (value != null) ? value : -1;

// Best practice: Always check before pop/peek!
04

Balanced Parentheses

This is the classic stack problem and one of the most common interview questions! The idea is simple: every opening bracket needs a matching closing bracket in the right order. Stacks are perfect for this because the most recent opening bracket must be closed first (LIFO).

Why Stacks Are Perfect for This

Consider the string "([{}])". The innermost bracket {} must close first, then [], then (). This is exactly LIFO order!

Visual: Valid Parentheses Check

Input: "([{}])" - Let's trace through step by step!

PUSHING OPENING BRACKETS
char '(' → Push!   Stack: [(]
char '[' → Push!   Stack: [(, []
char '{' → Push!   Stack: [(, [, {]

Opening brackets always get pushed!
MATCHING CLOSING BRACKETS
char '}' → Pop {  Match?   Stack: [(, []
char ']' → Pop [  Match?   Stack: [(]
char ')' → Pop (  Match?   Stack: []

Stack empty + all matched = VALID!
INVALID EXAMPLES
"([)]"Wrong order! Push (, Push [, See ) but top is [ → MISMATCH!

"((("Not closed! Stack still has [(, (, (] at end → NOT EMPTY!

")))("Extra close! Pop on empty stack for first ) → UNDERFLOW!
Three Conditions for Invalid: (1) Closing bracket doesn't match top of stack, (2) Stack not empty at end, (3) Pop from empty stack

This is the most straightforward approach and easiest to understand. We create a stack to hold opening brackets. As we scan through each character in the string, we decide whether to push it (if it's an opening bracket) or try to match it with what's on top of the stack (if it's a closing bracket).

// Valid Parentheses - LeetCode 20
// Check if string has balanced brackets: (), [], {}
boolean isValid(String s) {
    Deque stack = new ArrayDeque<>();
    
    for (char c : s.toCharArray()) {
        // Push opening brackets
        if (c == '(' || c == '[' || c == '{') {
            stack.push(c);
        }

When we encounter a closing bracket, we need to check two things: (1) Is there even an opening bracket waiting to be matched? If the stack is empty, we have an "orphan" closing bracket with no partner - that's invalid! (2) Does the closing bracket match the most recent opening bracket? We pop the top and compare.

        else {
            // Check for matching closing bracket
            if (stack.isEmpty()) return false;
            
            char top = stack.pop();
            if (c == ')' && top != '(') return false;
            if (c == ']' && top != '[') return false;
            if (c == '}' && top != '{') return false;
        }
    }

After processing all characters, we do one final check: is the stack empty? If there are leftover opening brackets in the stack, they never got matched with closing brackets - that means the string is invalid. Only if every opening bracket found its closing partner do we return true.

    return stack.isEmpty();  // All brackets matched
}

This approach uses a Map to store the bracket pairs, making the code more elegant and easier to extend. The map stores closing brackets as keys and their matching opening brackets as values. This way, we can look up what opening bracket should match any closing bracket we encounter.

// Alternative with map
boolean isValidV2(String s) {
    Deque stack = new ArrayDeque<>();
    Map map = Map.of(
        ')', '(',
        ']', '[',
        '}', '{'
    );

Now the logic becomes cleaner: if the character is an opening bracket (exists as a value in our map), push it. If it's a closing bracket (exists as a key), pop and verify the match in one line. The expression stack.pop() != map.get(c) checks if what we popped matches what this closing bracket expects.

    for (char c : s.toCharArray()) {
        if (map.containsValue(c)) {
            stack.push(c);
        } else if (map.containsKey(c)) {
            if (stack.isEmpty() || stack.pop() != map.get(c)) {
                return false;
            }
        }
    }
    
    return stack.isEmpty();
}

This is a clever variation! Instead of checking validity, we count how many brackets we'd need to INSERT to make the string valid. We track two counters: open for unmatched opening brackets, and close for unmatched closing brackets. When we see (, we increment open. When we see ), we try to match it with an existing (.

// Minimum Add to Make Parentheses Valid
// Count minimum insertions needed
int minAddToMakeValid(String s) {
    int open = 0;   // Unmatched '('
    int close = 0;  // Unmatched ')'
    
    for (char c : s.toCharArray()) {
        if (c == '(') {
            open++;
        } else {
            if (open > 0) {
                open--;  // Match with open
            } else {
                close++;  // Unmatched close
            }
        }
    }
    
    return open + close;  // Total unmatched brackets
}
Algorithm Walkthrough:
Valid Parentheses Logic:
  1. See (, [, or {? Push it
  2. See ), ], or }? Pop and compare
  3. If stack is empty when closing - Invalid
  4. If mismatch (e.g., ( vs ]) - Invalid
  5. End: stack must be empty - Valid
Example: "([{}])"
  • ( - Push - Stack: (
  • [ - Push - Stack: ([
  • { - Push - Stack: ([{
  • } - Pop { (match) - Stack: ([
  • ] - Pop [ (match) - Stack: (
  • ) - Pop ( (match) - Stack: empty (Valid)

Practice Questions: Balanced Parentheses

Task: Trace through the string "([)]" using the stack algorithm. Is it valid?

Show Solution
String: "([)]"

Step by step:
'(' - Opening, push → Stack: ['(']
'[' - Opening, push → Stack: ['(', '[']
')' - Closing, pop '[' ← MISMATCH!
    ')' expects '(' but got '['
    → INVALID!

Answer: NO, it's invalid.

The brackets are interleaved, not properly nested.
Valid would be: "([])" or "([])"
Think of it like HTML tags - they must nest properly.

Task: If we only have '(' and ')', do we still need a stack? Can we solve it with just a counter?

Show Solution
// With only ONE type of bracket, use a counter!
boolean isValid(String s) {
    int count = 0;
    
    for (char c : s.toCharArray()) {
        if (c == '(') count++;
        else if (c == ')') count--;
        
        // If count goes negative, closing without opening
        if (count < 0) return false;
    }
    
    return count == 0;  // All matched
}

// Why counter works for single type:
// - No need to match WHICH bracket
// - Just count: +1 for open, -1 for close
// - Stack needed only when matching types

Given: "(()" → Output: 2 (the substring "()")

Given: ")()())" → Output: 4 (the substring "()()")

Task: Find length of longest valid parentheses substring.

Hint: Store indices in the stack, not characters.

Show Solution
int longestValidParentheses(String s) {
    Deque stack = new ArrayDeque<>();
    stack.push(-1);  // Base for length calculation
    int maxLen = 0;
    
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '(') {
            stack.push(i);  // Push index
        } else {
            stack.pop();
            if (stack.isEmpty()) {
                stack.push(i);  // New base
            } else {
                maxLen = Math.max(maxLen, i - stack.peek());
            }
        }
    }
    return maxLen;
}
// Key insight: Stack stores INDICES
// Length = current index - last unmatched index
05

Expression Evaluation

How does your calculator know that 3 + 2 * 4 equals 11 (not 20)? It needs to handle operator precedence! Stacks make this elegant. Let's start with the simpler case: Reverse Polish Notation (RPN) where precedence is already resolved.

What is Reverse Polish Notation (Postfix)?

Instead of writing 3 + 4, we write 3 4 +. The operator comes after its operands. Why? No parentheses needed and precedence is built in!

Infix (Normal)
3 + 4

Operator between operands

Postfix (RPN)
3 4 +

Operator after operands

Prefix (Polish)
+ 3 4

Operator before operands

Visual: Evaluating RPN

Expression: ["2", "1", "+", "3", "*"] = ((2 + 1) * 3) = 9

STEP-BY-STEP
Token "2" (number) → Push 2     Stack: [2]
Token "1" (number) → Push 1     Stack: [2, 1]
Token "+" (operator):
  → Pop 1 (b), Pop 2 (a)
  → Calculate: a + b = 2 + 1 = 3Push 3             Stack: [3]
Token "3" (number) → Push 3     Stack: [3, 3]
Token "*" (operator):
  → Pop 3 (b), Pop 3 (a)
  → Calculate: a * b = 3 * 3 = 9Push 9             Stack: [9]
THE RULE
If number:
  → Push to stack

If operator:
  → Pop b (top)
  → Pop a (second)
  → Calculate a op b
  → Push result

Final answer:
  → Pop the stack!
Order Matters! For subtraction and division: a op b where a is popped second! "10 3 -" = 10 - 3 = 7 (not 3 - 10)

The main function loops through each token in the expression. If it's a number, we push it onto the stack. If it's an operator, we pop two values, perform the calculation, and push the result back. At the end, the final answer is the only value left on the stack.

// Evaluate Reverse Polish Notation (Postfix)
// ["2","1","+","3","*"] = ((2 + 1) * 3) = 9
int evalRPN(String[] tokens) {
    Deque stack = new ArrayDeque<>();
    
    for (String token : tokens) {
        if (isOperator(token)) {
            int b = stack.pop();  // Pop top (second operand)
            int a = stack.pop();  // Pop next (first operand)
            stack.push(calculate(a, b, token));
        } else {
            stack.push(Integer.parseInt(token));
        }
    }
    
    return stack.pop();  // Final result
}

We need a helper function to check if a token is an operator. This keeps our main logic clean and readable. We simply check if the string equals any of the four basic arithmetic operators.

private boolean isOperator(String s) {
    return s.equals("+") || s.equals("-") || 
           s.equals("*") || s.equals("/");
}

The calculate helper performs the actual arithmetic. Notice we use a op b order - this is critical! When we popped, b came first (it was on top), so a is actually the first operand in the original expression. For "10 3 -", we want 10 - 3, not 3 - 10.

private int calculate(int a, int b, String op) {
    switch (op) {
        case "+": return a + b;
        case "-": return a - b;
        case "*": return a * b;
        case "/": return a / b;
        default: throw new IllegalArgumentException();
    }
}

Basic Calculator II: Handling Infix Notation

Now for the harder problem: evaluating normal infix expressions like "3+2*2". The trick is handling operator precedence - multiplication and division happen before addition and subtraction. We use a clever approach: process * and / immediately, but delay + and - by pushing values to the stack.

// Basic Calculator II: "3+2*2" = 7
int calculate(String s) {
    Deque stack = new ArrayDeque<>();
    int num = 0;        // Current number being built
    char lastOp = '+';  // Previous operator (start with + for first number)

We iterate through each character. When we see a digit, we build up the current number (handling multi-digit numbers like "123" by multiplying by 10 and adding). We skip spaces entirely.

    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        
        if (Character.isDigit(c)) {
            num = num * 10 + (c - '0');  // Build multi-digit number
        }

When we hit an operator (or reach the end), we process the previous number based on the previous operator. For + we push the number, for - we push the negative. For * and / we pop, calculate immediately, and push the result. This ensures * and / are evaluated before + and -.

        if ((!Character.isDigit(c) && c != ' ') || i == s.length() - 1) {
            switch (lastOp) {
                case '+': stack.push(num); break;
                case '-': stack.push(-num); break;
                case '*': stack.push(stack.pop() * num); break;
                case '/': stack.push(stack.pop() / num); break;
            }
            lastOp = c;  // Save current operator for next iteration
            num = 0;     // Reset for next number
        }
    }

Finally, sum up everything in the stack. Because we pushed negative values for subtraction, a simple sum gives us the correct answer. For "3+2*2": we push 3, then push 4 (2*2 evaluated immediately), and 3+4 = 7.

    int result = 0;
    while (!stack.isEmpty()) {
        result += stack.pop();
    }
    return result;
}
Understanding RPN (Reverse Polish Notation):
How RPN Works:
  • Operands (numbers) → Push to stack
  • Operator (+, -, *, /) → Pop 2 operands, calculate, push result
  • Order matters: a op b where a is popped second!
Example: ["2","1","+","3","*"]
  • 2 → Stack: [2]
  • 1 → Stack: [2, 1]
  • + → Pop 1,2 → 2+1=3 → Stack: [3]
  • 3 → Stack: [3, 3]
  • * → Pop 3,3 → 3*3=9 → Result: 9

Practice Questions: Expression Evaluation

Task: Trace through and evaluate the RPN expression. What's the result?

Show Solution
RPN: ["4", "13", "5", "/", "+"]

Step by step:
"4"  → Push 4       → Stack: [4]
"13" → Push 13      → Stack: [4, 13]
"5"  → Push 5       → Stack: [4, 13, 5]
"/"  → Pop 5, 13    → 13 / 5 = 2
     → Push 2       → Stack: [4, 2]

"+"  → Pop 2, 4     → 4 + 2 = 6
     → Push 6       → Stack: [6]

Answer: 6

Note: Division is 13/5 = 2 (integer division)
Order: second popped / first popped

Task: Trace through "3+2*2" using the Basic Calculator II algorithm. Show how operator precedence is handled.

Show Solution
String: "3+2*2"
Variables: num=0, lastOp='+'

i=0, c='3': num = 3
i=1, c='+': 
  lastOp='+' → push(3) → Stack: [3]
  lastOp = '+', num = 0
i=2, c='2': num = 2  
i=3, c='*':
  lastOp='+' → push(2) → Stack: [3, 2]
  lastOp = '*', num = 0
i=4, c='2': num = 2 (end of string triggers process)
  lastOp='*' → pop()*2 = 2*2 = 4 → push(4)
  Stack: [3, 4]

Sum stack: 3 + 4 = 7

Key insight: * and / are processed IMMEDIATELY
while + and - just push values to stack.
This naturally handles precedence!

Task: Convert the infix expression "3+(2*4)" to postfix notation using the Shunting Yard algorithm.

Hint: Use a stack for operators. Output numbers immediately.

Show Solution
Infix: "3+(2*4)"

Shunting Yard Algorithm:
Output queue: (result)
Operator stack: (temp storage)

'3' → number → Output: [3]
'+' → operator → Stack: [+]
'(' → open paren → Stack: [+, (]
'2' → number → Output: [3, 2]
'*' → operator → Stack: [+, (, *]
'4' → number → Output: [3, 2, 4]
')' → close paren → pop until '('
     → pop '*' → Output: [3, 2, 4, *]
     → pop '(' (discard)
     → Stack: [+]
End → pop remaining → Output: [3, 2, 4, *, +]

Postfix result: "3 2 4 * +"

Verify: 2*4=8, then 3+8=11 ✓
06

Monotonic Stack

This is where stacks get really powerful! A monotonic stack maintains elements in sorted order - either always increasing or always decreasing from bottom to top. It's a magical technique for solving "next greater/smaller" problems in O(n) instead of O(n²)!

Monotonic Stack

A stack that maintains elements in sorted order (either increasing or decreasing). Useful for finding next greater/smaller element problems in O(n) time.

When to Use Monotonic Stack?

Whenever you see these keywords in a problem, think monotonic stack:

"Next Greater Element"

For each element, find the first larger element to its right

"Next Smaller Element"

For each element, find the first smaller element to its right

"Previous Greater/Smaller"

Same idea, but looking to the left instead

Visual: Next Greater Element with Monotonic Stack

Input: [2, 1, 2, 4, 3] → Find next greater element for each

STEP-BY-STEP
i=0: Push 0     Stack: [0]       (index of 2)
i=1: 1 < 2?     Stack: [0,1]     (push 1)
i=2: 2 > 1?     Pop 1! result[1]=2
     2 ≤ 2?     Stack: [0,2]     (push 2)
i=3: 4 > 2?     Pop 2! result[2]=4
     4 > 2?     Pop 0! result[0]=4
     Stack: [3]         (push 3)
i=4: 3 < 4?     Stack: [3,4]     (push 4)
RESULT
Index:  0   1   2   3   4
Input:  2   1   2   4   3
Result: 4   2   4  -1  -1

Explanation:
• 2 → next greater is 4
• 1 → next greater is 2
• 2 → next greater is 4
• 4 → no greater to right → -1
• 3 → no greater to right → -1
The Magic: We store indices (not values) in the stack. When we find a greater element, we pop and record the answer. Elements remaining in stack have no greater element!

This is the classic monotonic stack problem. For each element in the array, we want to find the first element to its right that is greater. We use a stack to store indices of elements that haven't found their "next greater" yet.

// Next Greater Element
// For each element, find next greater element to its right
int[] nextGreaterElement(int[] nums) {
    int n = nums.length;
    int[] result = new int[n];
    Arrays.fill(result, -1);  // Default: no greater element
    
    Deque stack = new ArrayDeque<>();  // Store indices

As we iterate through the array, we check if the current element is greater than elements represented by indices in our stack. If yes, we found the answer for those indices! We pop them and record the answer. Then we push the current index.

    for (int i = 0; i < n; i++) {
        // Pop all smaller elements - nums[i] is their answer
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
            int idx = stack.pop();
            result[idx] = nums[i];
        }
        stack.push(i);
    }
    
    return result;
}

This is a variation of next greater element. Instead of finding the greater value, we need to find how many days until a warmer temperature. The key difference: we return the number of days (i - idx) instead of the temperature value.

// Daily Temperatures
// How many days until warmer temperature?
int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] result = new int[n];
    Deque stack = new ArrayDeque<>();

Same pattern as before: when we find a warmer day, we pop indices from the stack and calculate the day difference. The beauty is that storing indices (not values) lets us compute both the temperature comparison AND the day count.

    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && 
               temperatures[stack.peek()] < temperatures[i]) {
            int idx = stack.pop();
            result[idx] = i - idx;  // Days until warmer
        }
        stack.push(i);
    }
    
    return result;
}

This is a harder problem that uses a monotonic increasing stack. We want to find the largest rectangle that can be formed in a histogram. The trick is: for each bar, we calculate the maximum rectangle where that bar is the shortest.

// Largest Rectangle in Histogram
int largestRectangleArea(int[] heights) {
    int n = heights.length;
    Deque stack = new ArrayDeque<>();
    int maxArea = 0;

We iterate through bars plus one extra (with height 0 to flush remaining bars). When we see a shorter bar, we pop and calculate the area. The width extends from the current position back to the previous bar in stack (or to the beginning if stack is empty).

    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i];
        
        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;
}

Practice Questions: Monotonic Stack

Task: For each element, find the next greater element to its right. Return -1 if none exists.

Show Solution
Input: [4, 5, 2, 10, 8]

i=0, val=4: Push 0      Stack: [0]
i=1, val=5: 
  5 > nums[0]=4? Yes! → result[0]=5, pop
  Push 1              Stack: [1]
i=2, val=2:
  2 > nums[1]=5? No
  Push 2              Stack: [1, 2]
i=3, val=10:
  10 > nums[2]=2? Yes! → result[2]=10, pop
  10 > nums[1]=5? Yes! → result[1]=10, pop
  Push 3              Stack: [3]
i=4, val=8:
  8 > nums[3]=10? No
  Push 4              Stack: [3, 4]

Remaining in stack → -1 (no greater)

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

Task: Explain when to use monotonic increasing stack vs monotonic decreasing stack. Give examples.

Show Solution
Monotonic DECREASING stack (top is smallest):
- Used for: Next Greater Element
- Pop when: current > stack top
- Stack maintains: decreasing order
- Example: Daily Temperatures, Next Greater

Monotonic INCREASING stack (top is largest):
- Used for: Next Smaller Element
- Pop when: current < stack top  
- Stack maintains: increasing order
- Example: Next Smaller Element

Memory trick:
"Next GREATER" → DECREASING stack
  (we pop smaller elements when we find greater)
"Next SMALLER" → INCREASING stack
  (we pop larger elements when we find smaller)

Histogram problem uses INCREASING (pop taller bars
when we see a shorter one)

Task: How many days until warmer temperature for each day? Return 0 if no warmer day exists.

Show Solution
Input: [73, 74, 75, 71, 69, 72, 76, 73]
Index:  0   1   2   3   4   5   6   7

i=0: Push 0           Stack: [0]
i=1: 74>73 → result[0]=1-0=1, pop
     Push 1           Stack: [1]
i=2: 75>74 → result[1]=2-1=1, pop
     Push 2           Stack: [2]
i=3: 71<75, Push 3    Stack: [2,3]
i=4: 69<71, Push 4    Stack: [2,3,4]
i=5: 72>69 → result[4]=5-4=1, pop
     72>71 → result[3]=5-3=2, pop
     72<75, Push 5    Stack: [2,5]
i=6: 76>72 → result[5]=6-5=1, pop
     76>75 → result[2]=6-2=4, pop
     Push 6           Stack: [6]
i=7: 73<76, Push 7    Stack: [6,7]

Remaining → 0 (no warmer)

Answer: [1, 1, 4, 2, 1, 1, 0, 0]

Key Takeaways

LIFO Principle

Last In, First Out. All operations at the top - push, pop, peek are O(1)

Use ArrayDeque

Prefer Deque with ArrayDeque over legacy Stack class for better performance

Matching Problems

Stacks excel at matching pairs - parentheses, tags, undo operations

Monotonic Stack

For next greater/smaller element problems - O(n) instead of O(n squared)

Expression Evaluation

Reverse Polish Notation uses stacks - push numbers, pop for operators

Always Check isEmpty

Prevent underflow errors by checking isEmpty before pop or peek

Knowledge Check

Question 1 of 6

What does LIFO stand for?

Question 2 of 6

Which Java class is preferred for implementing a Stack?

Question 3 of 6

What is a monotonic stack used for?

Question 4 of 6

In the valid parentheses problem, when do we push to the stack?

Question 5 of 6

What is the time complexity of push and pop operations in a stack?

Question 6 of 6

Which data structure is used for function call management in programs?