Last in, first out. The data structure running behind every function call your code makes.
โฌก BeginnerA 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.
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.
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.
When you write this code:
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:
sum_of_squares(3, 4) is pushedsquare(3) โ pushed on topsquare(3) calls multiply(3, 3) โ pushed on topmultiply returns 9 โ poppedsquare returns 9 โ poppedsquare(4) is called โ pushedsquare(4) calls multiply(4, 4) โ pushedmultiply returns 16 โ poppedsquare returns 16 โ poppedsum_of_squares returns 9 + 16 = 25 โ poppedEvery 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 (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.
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.
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.
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.
| 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.
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.
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
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
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
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.
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]
is_empty() before calling pop() or peek(). In interviews, edge-testing the empty case is the first thing your interviewer will look for.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.Every text editor, design tool, and spreadsheet has undo/redo. Here's how it works with two stacks:
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.
Stack problems are about recognizing when LIFO order matches the problem's structure. Here are the recurring patterns.
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).
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.
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.
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.
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.
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).
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.
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 |