On This Page

What Is a Stack?

A stack is a collection where you can only add and remove elements from the top. The last thing you put in is the first thing you take out โ€” LIFO (Last In, First Out).

Think of a stack of plates in a cafeteria. You put a clean plate on top. When someone needs a plate, they grab the top one. You never pull from the middle or bottom โ€” just the top. That's the entire interface: push onto the top, pop from the top, peek at the top.

Three operations. That's it. The simplicity is the point. By restricting what you can do, a stack makes certain problems dramatically easier to solve.

A Brief History

The stack as a computing concept was first proposed by Alan Turing in 1946, when he described a "bury and unbury" mechanism for subroutine calls. The term "stack" was coined by the Australian computer scientist Charles Hamblin in 1957. Friedrich Bauer and Klaus Samelson filed the first patent for a stack-based architecture in 1957, and Bauer received the IEEE Computer Pioneer Award in 1988 for this work.

The concept became central to computing when Edsger Dijkstra popularized the use of stacks for expression parsing in the late 1950s. His "shunting-yard algorithm" (named after railroad yard switching) converts mathematical expressions from infix notation (how humans write: 3 + 4 * 2) to postfix notation (how machines evaluate: 3 4 2 * +) using a stack. Every calculator and compiler since then uses some variation of this approach.

Where Stacks Show Up Everywhere

Stack vs. Array

A stack isn't a different data structure from an array โ€” it's an array (or linked list) with a restricted interface. You can implement a stack with a dynamic array where push is append and pop removes the last element. The restriction is the whole point: you promise to only use LIFO operations, and that constraint is what makes stack-based algorithms work.

Analogy: a stack is to an array what a TV remote is to a universal remote. The TV remote has fewer buttons, but that's what makes it useful โ€” you can't accidentally change the input or set a sleep timer when you just want to change the channel.

How It Works

The Call Stack โ€” Your Computer's Most Important Stack

When you write this code:

Python
def multiply(a, b):
    return a * b

def square(n):
    return multiply(n, n)

def sum_of_squares(x, y):
    return square(x) + square(y)

result = sum_of_squares(3, 4)  # โ†’ 25

The call stack grows and shrinks like this:

  1. sum_of_squares(3, 4) is pushed
  2. It calls square(3) โ€” pushed on top
  3. square(3) calls multiply(3, 3) โ€” pushed on top
  4. multiply returns 9 โ€” popped
  5. square returns 9 โ€” popped
  6. Now square(4) is called โ€” pushed
  7. square(4) calls multiply(4, 4) โ€” pushed
  8. multiply returns 16 โ€” popped
  9. square returns 16 โ€” popped
  10. sum_of_squares returns 9 + 16 = 25 โ€” popped

Every recursive algorithm works this way. Each recursive call pushes a frame; each return pops one. That's why you can always convert recursion to an explicit stack โ€” they're the same mechanism.

Array-Based vs. Linked List-Based

Array-based (the common choice): Push appends to the end. Pop removes the last element. Both O(1). The array might need to resize occasionally, but amortized (averaged over many operations) cost is still O(1). This is what Python's list and JavaScript's Array give you out of the box.

Linked list-based: Push inserts at the head. Pop removes the head. Both O(1) worst case (no amortized caveat). Uses more memory per element because of the pointer overhead, and worse cache locality (the CPU can't prefetch scattered nodes). Rarely used in practice unless you need strict O(1) guarantees with no occasional spikes.

Edge Cases to Watch

Empty stack: Popping or peeking an empty stack should throw an error, not return garbage. Always check is_empty() first, or use exception handling.

Single element: After pushing one element and popping it, the stack should be empty. Sounds obvious, but bugs here are common in custom implementations โ€” check that your size counter and top pointer both reset correctly.

โš ๏ธ Stack Overflow

The call stack has a fixed size (typically 1-8 MB depending on OS and language). If you recurse too deeply โ€” say, processing a linked list of 100,000 nodes recursively โ€” you'll blow through the limit and crash. Python's default recursion limit is 1,000 (check with sys.getrecursionlimit()). The fix: convert to an iterative approach with an explicit stack. You get the same behavior without the memory limit.

Interactive Visualization

Push values onto the stack and pop them off. Watch the LIFO order in action. Toggle "Call Stack" to see a simulated function call stack.

Stack Operations

Operations & Complexity

Operation Time Notes
Push O(1)* *Amortized for array-based; strict O(1) for linked list
Pop O(1) Remove and return top element
Peek / Top O(1) Look at top without removing
isEmpty O(1) Check if size is zero
Search O(n) Not a standard stack operation โ€” have to pop to search
Access by index O(n) Not a standard operation โ€” stacks don't support random access

Space complexity: O(n) โ€” one slot per element in the stack.

Implementation

In most languages you'd just use the built-in list/array as a stack. But implementing one from scratch shows you there's nothing magical about it.

Stack in Python

Python
class Stack:
    def __init__(self):
        self._data = []  # Python list handles resizing for us

    def push(self, val):
        self._data.append(val)  # O(1) amortized

    def pop(self):
        if self.is_empty():
            raise IndexError("pop from empty stack")
        return self._data.pop()  # O(1) โ€” removes last element

    def peek(self):
        if self.is_empty():
            raise IndexError("peek at empty stack")
        return self._data[-1]  # O(1) โ€” just look, don't remove

    def is_empty(self):
        return len(self._data) == 0

    def __len__(self):
        return len(self._data)

    def __repr__(self):
        # Show top of stack on the right for clarity
        return f"Stack({self._data})"


# Example: balanced parentheses checker
def is_balanced(s):
    """Check if brackets in string are properly nested."""
    stack = Stack()
    # Map each closer to its opener โ€” lets us handle all three
    # bracket types in one pass without separate if/elif chains
    pairs = {')': '(', ']': '[', '}': '{'}

    for char in s:
        if char in '([{':
            stack.push(char)
        elif char in ')]}':
            # Two failure modes: nothing to match, or wrong match
            if stack.is_empty() or stack.pop() != pairs[char]:
                return False
    # If anything's left, we have unclosed openers
    return stack.is_empty()


# Usage
print(is_balanced("({[]})"))    # True
print(is_balanced("([)]"))      # False โ€” interleaved
print(is_balanced("(("))        # False โ€” unclosed

Stack in JavaScript

JavaScript
class Stack {
  constructor() {
    this._data = [];
  }

  push(val) {
    this._data.push(val);
  }

  pop() {
    if (this.isEmpty()) throw new Error("Stack underflow");
    return this._data.pop();
  }

  peek() {
    if (this.isEmpty()) throw new Error("Stack is empty");
    return this._data[this._data.length - 1];
  }

  isEmpty() {
    return this._data.length === 0;
  }

  get size() {
    return this._data.length;
  }
}

// Example: evaluate postfix expression (Reverse Polish Notation)
// "3 4 + 2 *" means (3 + 4) * 2 = 14
function evalPostfix(expr) {
  const stack = new Stack();
  const tokens = expr.split(" ");

  for (const token of tokens) {
    if ("+-*/".includes(token)) {
      // Pop two operands โ€” note the order matters for - and /
      const b = stack.pop();  // second operand was pushed last
      const a = stack.pop();  // first operand was pushed first
      switch (token) {
        case "+": stack.push(a + b); break;
        case "-": stack.push(a - b); break;
        case "*": stack.push(a * b); break;
        case "/": stack.push(Math.trunc(a / b)); break;
      }
    } else {
      stack.push(Number(token));
    }
  }
  return stack.pop();
}

console.log(evalPostfix("3 4 + 2 *")); // 14
console.log(evalPostfix("5 1 2 + 4 * + 3 -")); // 14

Min Stack (Bonus โ€” Common Interview Problem)

Python
class MinStack:
    """Stack that supports getMin() in O(1) time.
    
    The trick: maintain a parallel stack that tracks the minimum
    value at each "level" of the main stack. When we push a value
    that's <= the current minimum, we push it onto the min stack too.
    When we pop a value equal to the current minimum, we pop from
    the min stack as well.
    """

    def __init__(self):
        self._data = []
        self._mins = []  # Parallel stack tracking the minimum at each level

    def push(self, val):
        self._data.append(val)
        # If this value is <= current min, push it onto mins too
        # Using <= (not <) handles duplicate minimums correctly
        if not self._mins or val <= self._mins[-1]:
            self._mins.append(val)

    def pop(self):
        val = self._data.pop()
        # If we're popping the current minimum, pop from mins too
        if val == self._mins[-1]:
            self._mins.pop()
        return val

    def top(self):
        return self._data[-1]

    def get_min(self):
        return self._mins[-1]  # O(1) โ€” always the current minimum

Variation: Monotonic Stack

A monotonic stack maintains elements in sorted order (either increasing or decreasing from bottom to top). When you push a new element, you first pop everything that violates the ordering. This gives you the "next greater element" or "next smaller element" for every position in O(n) total time.

Python
def next_greater_element(nums):
    """For each element, find the next element that is larger.
    
    Uses a monotonic decreasing stack (from bottom to top).
    When we encounter a number larger than the stack's top,
    that number is the "next greater" for everything smaller
    on the stack.
    
    Example: [4, 2, 1, 5, 3] โ†’ [5, 5, 5, -1, -1]
    
    Time: O(n) โ€” each element is pushed and popped at most once
    Space: O(n) โ€” for the stack and result array
    """
    n = len(nums)
    result = [-1] * n
    stack = []  # stores indices, not values

    for i in range(n):
        # Pop all elements smaller than current โ€” current is their answer
        while stack and nums[stack[-1]] < nums[i]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)

    return result

# Example: Daily Temperatures (LC #739)
# Given temperatures, find how many days until a warmer day
def daily_temperatures(temps):
    """Same monotonic stack idea, but return days-to-wait."""
    n = len(temps)
    answer = [0] * n
    stack = []

    for i in range(n):
        while stack and temps[stack[-1]] < temps[i]:
            idx = stack.pop()
            answer[idx] = i - idx  # days between
        stack.append(i)

    return answer

print(daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]))
# โ†’ [1, 1, 4, 2, 1, 1, 0, 0]

Common Mistakes

โš ๏ธ Mistakes That Trip Up Beginners
  1. Popping from an empty stack. This is the stack equivalent of array index out of bounds. Always check is_empty() before calling pop() or peek(). In interviews, edge-testing the empty case is the first thing your interviewer will look for.
  2. Confusing LIFO with FIFO. A stack gives you the most recent item (LIFO). A queue gives you the oldest item (FIFO). If your algorithm needs elements in arrival order, you want a queue, not a stack. BFS = queue. DFS = stack.
  3. Using recursion when iteration + stack is safer. Deep recursion (1,000+ frames in Python, 10,000+ in Java) causes stack overflow. If the input can be large, use an explicit stack instead. The logic is identical โ€” you're just managing the stack yourself.
  4. Getting operand order wrong in expression evaluation. When evaluating 3 - 1 in postfix, you pop 1 first (it was pushed last) and 3 second. The operation is 3 - 1 = 2, not 1 - 3 = -2. The first popped value is the second operand.
  5. Not recognizing monotonic stack problems. When a problem asks for "next greater element," "largest rectangle," or "stock span," people reach for nested loops (O(nยฒ)). The monotonic stack gives O(n). If you see "next greater/smaller" in the problem, think monotonic stack immediately.

Real-World Example: Undo/Redo System

Every text editor, design tool, and spreadsheet has undo/redo. Here's how it works with two stacks:

Python
class UndoRedoEditor:
    """Simple text editor with undo/redo support.
    
    Two stacks:
    - undo_stack: holds previous states (push on every edit)
    - redo_stack: holds states you've undone (push on undo)
    
    Key rule: any new edit clears the redo stack.
    Why? Once you edit after undoing, the "future" you undid
    is no longer valid.
    """
    def __init__(self):
        self.content = ""
        self.undo_stack = []
        self.redo_stack = []

    def type_text(self, text):
        """Add text at the end."""
        self.undo_stack.append(self.content)  # Save current state
        self.redo_stack.clear()                # New edit kills redo
        self.content += text

    def delete_last(self, n):
        """Delete last n characters."""
        self.undo_stack.append(self.content)
        self.redo_stack.clear()
        self.content = self.content[:-n] if n < len(self.content) else ""

    def undo(self):
        """Revert to previous state."""
        if not self.undo_stack:
            return  # Nothing to undo
        self.redo_stack.append(self.content)  # Save current for redo
        self.content = self.undo_stack.pop()   # Restore previous

    def redo(self):
        """Re-apply the last undone action."""
        if not self.redo_stack:
            return  # Nothing to redo
        self.undo_stack.append(self.content)
        self.content = self.redo_stack.pop()


# Demo
editor = UndoRedoEditor()
editor.type_text("Hello")
editor.type_text(" World")
print(editor.content)   # "Hello World"

editor.undo()
print(editor.content)   # "Hello"

editor.undo()
print(editor.content)   # ""

editor.redo()
print(editor.content)   # "Hello"

editor.type_text("!")
print(editor.content)   # "Hello!"
# redo_stack is now empty โ€” the " World" future is gone

This exact pattern powers Ctrl+Z in VSCode, Google Docs, Photoshop, and every other tool with undo. The two-stack approach gives O(1) undo and O(1) redo. Some advanced editors use a tree instead of a linear stack (so you can branch into multiple undo histories), but the two-stack version covers 99% of use cases.

Interview Patterns

Stack problems are about recognizing when LIFO order matches the problem's structure. Here are the recurring patterns.

๐Ÿ’ก Matching / Balancing

Any time you need to match nested pairs โ€” parentheses, HTML tags, nested structures โ€” a stack is the go-to. Push when you see an opener, pop when you see a closer. If the stack is empty at the end and every pop was a valid match, you're good.

Problems: Valid Parentheses (#20), Decode String (#394), Basic Calculator (#224).

๐Ÿ’ก Monotonic Stack

A stack where elements are kept in sorted order (either increasing or decreasing). When you push a new element, you first pop everything that violates the ordering. The magic: this gives you the "next greater element" or "next smaller element" for every position in O(n) total time.

This is the #1 non-obvious stack pattern. If a problem asks about "next greater/smaller" or "largest rectangle," think monotonic stack immediately.

๐Ÿ’ก Converting Recursion to Iteration

Any recursive algorithm can be rewritten with an explicit stack. The recursive call stack and your explicit stack do the exact same thing โ€” push a "frame" with the current state, then process. This matters when the input is large enough to cause a stack overflow.

Pattern: replace the recursive call with a push. Replace the base case with the pop-and-process loop. The order of pushes determines DFS vs. BFS behavior.

๐Ÿ’ก Expression Evaluation

Calculators, compilers, and many interview problems use two stacks: one for operators and one for operands. When you see a number, push it. When you see an operator, compare precedence with the stack's top operator. Higher or equal? Pop and compute before pushing the new one.

The Shunting Yard algorithm (Dijkstra, 1961) formalizes this into infix-to-postfix conversion.

๐Ÿ’ก Undo / State History

When you need to track previous states and roll back โ€” browser history, text editor undo, transaction rollback โ€” a stack gives you the most recent state instantly. Some problems pair two stacks: one for undo, one for redo.

๐Ÿ’ก String Manipulation with Stack

Some string problems hide a stack in disguise. "Remove all adjacent duplicates" โ€” push characters, pop when the top matches the current character. "Simplify Unix path" โ€” split by /, push directory names, pop on .., ignore . and empty segments.

Problems: Remove All Adjacent Duplicates (#1047), Simplify Path (#71), Backspace String Compare (#844).

๐Ÿ’ก Stack as Auxiliary Memory

Sometimes a stack serves as temporary storage while you rearrange elements. "Sort a stack using only another stack" โ€” pop from the input, find the right position in the output stack by temporarily moving elements back. "Implement queue with stacks" โ€” two stacks simulate FIFO order.

These problems test your understanding of LIFO constraints and how to work around them creatively.

Practice Problems

Start with #20 โ€” it's the warm-up that teaches you the core pattern. Then work through the rest. The monotonic stack problems (#739, #84) are the ones that separate good from great.

# Problem Difficulty Key Pattern
20 Valid Parentheses Easy Matching / balancing
155 Min Stack Medium Auxiliary min-tracking stack
150 Evaluate Reverse Polish Notation Medium Expression evaluation
739 Daily Temperatures Medium Monotonic stack (next greater)
394 Decode String Medium Nested matching + state save
71 Simplify Path Medium Stack for directory navigation
84 Largest Rectangle in Histogram Hard Monotonic stack
224 Basic Calculator Hard Two stacks + precedence