Part 6 of 30 in Data Structures & Algorithms: The Complete Course
Stacks Explained: LIFO, Operations, and the Problems Stacks Solve
The stack data structure from scratch: LIFO intuition, push/pop/peek in O(1), array vs linked-list implementations, and the classic applications — balanced parentheses, undo, expression evaluation, monotonic stacks.
The stack is the simplest structure in this course and one of the most consequential: your program is running on one right now (the call stack — next-lesson-but-one's subject). A stack is any collection restricted to one end: you push items onto the top, pop them off the top, and peek at the top without removing it. That restriction — Last In, First Out (LIFO) — sounds limiting, but it precisely matches every problem involving nesting, reversal, or 'return to the most recent unfinished thing'.
In plain words: the plate stack analogy
Picture a stack of plates in a cafeteria. The kitchen adds freshly washed plates to the top; diners take from the top. If you want the plate at the very bottom, you must first remove every plate above it — that's the contract. When a problem has that shape (the last thing opened is the first that must close), a stack isn't just *an* option, it's *the* structure. Nesting is inherently LIFO: the most recently opened bracket must be the first closed, the most recently called function is the first to return.
Plate C ← TOP (most recently pushed)
─────────
Plate B
─────────
Plate A ← BOTTOM (pushed first)
─────────
push(D) → Plate D ← new TOP
Plate C
Plate B
Plate A
pop() → removes Plate D, returns "D"
TOP is now Plate C againThe interface and its costs
| Operation | Meaning | Complexity |
|---|---|---|
| push(x) | Add x to the top | O(1) (amortized on a dynamic array) |
| pop() | Remove and return the top element | O(1) |
| peek() / top() | Return the top without removing it | O(1) |
| isEmpty() | Is the stack empty? | O(1) |
| size() | Number of elements | O(1) |
| search for a value | Not part of the stack contract | O(n) — and a design smell |
Every core operation is O(1). The stack's power lies entirely in the discipline it enforces, not in algorithmic cleverness. That last row is important: if you find yourself searching a stack's interior, you're fighting the abstraction and almost certainly want a different structure.
Two implementations: array vs linked list
class Stack:
data = [] # dynamic array
push(x):
data.append(x) # O(1) amortized — O(n) only on rare resize
pop():
if isEmpty(): raise Error("underflow")
return data.removeLast() # O(1)
peek():
if isEmpty(): raise Error("empty")
return data[data.length - 1] # O(1)
isEmpty():
return data.length == 0class Node:
value
next = null
class Stack:
head = null # top of the stack
count = 0
push(x):
node = new Node(x)
node.next = head
head = node
count++ # O(1) — insert at head
pop():
if isEmpty(): raise Error("underflow")
value = head.value
head = head.next
count--
return value # O(1) — delete at head
peek():
return head.value- Dynamic array (the usual choice): push = append, pop = remove-last — cache-friendly, compact, amortized O(1). Occasional resize doubles capacity in O(n) but happens geometrically rarely.
- Singly linked list: push/pop at the head — guaranteed worst-case O(1) with no resize pauses, at the cost of per-node pointer overhead and poorer cache behavior.
Default to the array
Unless you need hard worst-case O(1) (real-time systems) or your language hands you a linked structure anyway, the array-backed stack wins in practice. Contiguous memory beats pointers when both have the same Big-O — a theme that repeats throughout the course.
Classic application 1: balanced brackets
Is the string '([]{})' properly nested? '([)]' is not — the pairs interleave illegally. This is the canonical stack problem, executed by your editor, compiler, and JSON parser on every keystroke.
function isBalanced(s):
stack = empty Stack
pairs = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in {'(', '[', '{'}:
stack.push(ch)
elif ch in {')', ']', '}'}:
if stack.isEmpty():
return false # closer with no opener
top = stack.pop()
if top != pairs[ch]:
return false # mismatched type, e.g. '([)]'
return stack.isEmpty() # true only if all openers closedCharacter Action Stack (bottom → top)
───────── ──────────────── ────────────────────
( push ( )
[ push ( [
) pop → '[' ≠ '(' MISMATCH → return false
Result: NOT balanced ✗
Trace — '([{}])'
Character Action Stack
───────── ────────────── ─────────────────────
( push (
[ push ( [
{ push ( [ {
} pop '{' ✓ ( [
] pop '[' ✓ (
) pop '(' ✓ (empty)
End isEmpty? yes → return true ✓Classic application 2: expression evaluation (RPN & shunting-yard)
Postfix notation (RPN) writes operators after their operands: '3 4 + 2 ×' means (3 + 4) × 2. Evaluation is one stack pass: push numbers; on an operator, pop two operands, apply, push the result. The final stack holds one value — the answer.
Token Action Stack (bottom → top)
────── ───────────────────────── ───────────────────
3 push 3 3
4 push 4 3 4
+ pop 4 and 3, push 3+4=7 7
2 push 2 7 2
× pop 2 and 7, push 7×2=14 14
Result: 14 ✓
Handles operator precedence automatically — no parentheses needed.Infix expressions ('3 + 4 × 2') are converted to postfix with Dijkstra's shunting-yard algorithm: one stack for operators (popped by precedence), one output stream. Together these two passes are, in essence, how calculators and compiler front-ends evaluate arithmetic.
Classic application 3: undo, redo, and browser history
Every user action pushes its inverse onto an undo stack; Ctrl+Z pops and applies. Redo is a second stack — each undo pushes onto redo, and a fresh action clears the redo stack (you can't redo into a divergent timeline). Browser navigation is identical with page URLs instead of text edits.
undoStack = []
redoStack = []
perform(action):
apply(action)
undoStack.push(action)
redoStack.clear() # diverged — no going "forward"
undo():
if undoStack.isEmpty(): return
action = undoStack.pop()
unapply(action)
redoStack.push(action)
redo():
if redoStack.isEmpty(): return
action = redoStack.pop()
apply(action)
undoStack.push(action)The monotonic stack — the interview power tool
A monotonic stack keeps its contents sorted by evicting anything that would violate the order before each push. It solves the entire family of 'nearest greater/smaller element' problems in O(n) that look O(n²) at first glance.
function nextGreater(arr):
n = len(arr)
result = [-1] * n # default: no greater element
stack = [] # stack of indices, values decreasing
for i in 0..n-1:
# while the current element beats the stack top's value,
# the current element IS the answer for that index
while stack is not empty and arr[i] > arr[stack.top()]:
idx = stack.pop()
result[idx] = arr[i]
stack.push(i)
# indices remaining on the stack have no greater element → -1
return resulti=0 arr[0]=2 stack empty, push 0 stack: [0] (values [2])
i=1 arr[1]=1 1 < 2, push 1 stack: [0,1] (values [2,1])
i=2 arr[2]=5 5>arr[1]=1 → pop 1, result[1]=5
5>arr[0]=2 → pop 0, result[0]=5
push 2 stack: [2] (values [5])
i=3 arr[3]=3 3 < 5, push 3 stack: [2,3] (values [5,3])
End: indices 2,3 remain → result[2]=-1, result[3]=-1
Final: result = [5, 5, -1, -1]
arr = [2, 1, 5, 3]Problems that are this monotonic-stack template in disguise: daily temperatures (days until warmer), stock span (consecutive prior days ≤ today's price), largest rectangle in histogram, trapping rain water. Recognizing the pattern is the skill — the code is almost identical each time.
Under the hood: complexity, invariants, and edge cases
| Operation | Array stack | Linked-list stack | Notes |
|---|---|---|---|
| push | O(1) amortized | O(1) worst-case | Array resizes on capacity; linked list never does |
| pop | O(1) | O(1) | Both remove the top in constant time |
| peek | O(1) | O(1) | No removal — just read |
| Space | O(n) | O(n) | Linked list pays pointer overhead per node |
| Cache | Excellent | Poor | Array elements are contiguous; pointer chasing on linked list |
Edge cases every stack implementation must handle: underflow (pop or peek on an empty stack — must raise an error or return a sentinel); unbounded growth (a stack tracking open brackets or recursion frames can grow without limit if the input is pathological — consider a max-size check in resource-constrained settings); thread safety (a bare array/list stack is not thread-safe; use a concurrent stack or external lock if shared across threads).
Common pitfalls
- Popping an empty stack. Always guard with isEmpty(). Half of all first-attempt stack solutions crash here — it's the segfault/exception of stack code.
- Forgetting the end-of-input check. In bracket matching, a non-empty stack at the end means unclosed openers. Passing the per-character checks is not enough.
- Using index access on a stack (stack[i]). You don't have a stack problem if you need interior access. Reconsider the structure.
- Missing the monotonic-stack signal. Phrases like 'next greater', 'previous smaller', 'how many days until' are the tell. Nested loops there leave O(n) on the table.
- Confusing the two-stack undo model. Forgetting to clear the redo stack on a fresh action allows redoing into a diverged history.
When to use stacks — and variations
- Use stacks when: the problem involves nesting (brackets, HTML tags, recursive structure), returning to 'the most recent state' (undo, backtracking, DFS), or processing things in reverse arrival order.
- Min-stack variation: augment the stack with a parallel stack of running minimums so peek_min() is O(1). Every push records min(new_value, current_min); every pop removes both entries. Used in 'design a stack with getMin() in O(1)' — a classic interview question.
- Two-stack queue: simulate a queue with two stacks (inbox and outbox). Enqueue pushes to inbox; dequeue pops from outbox, refilling outbox from inbox when empty. All operations are amortized O(1) — a useful teaching bridge before Lesson 7.
Practice problems
- Balanced parentheses: given a string of brackets, return true iff every opener is matched and properly nested. Extend it to detect the *position* of the first mismatch.
- Daily temperatures: given an array of daily temperatures, for each day find how many days until a warmer temperature (or 0 if none). Solve in O(n) with a monotonic stack.
- Min-stack: design a stack that supports push, pop, top, and getMin — all in O(1) time. No built-in min data structures allowed.
Key takeaways
- A stack is LIFO discipline: push, pop, peek — all O(1). The plate-stack analogy makes the contract concrete and memorable.
- Array-backed stacks are the practical default; linked-list stacks give worst-case O(1) at a cache cost.
- Nesting (brackets, parsers), reversal, and 'return to most recent state' (undo, back buttons, DFS) are inherently stack-shaped problems.
- Postfix evaluation and shunting-yard turn arithmetic into two clean stack passes.
- The monotonic stack solves nearest-greater/smaller families in amortized O(n) — one of the highest-value interview patterns per line of code.
The stack's sibling flips the discipline: First In, First Out. That's the queue — next lesson, along with its two-ended cousin the deque. Then Lesson 8 reveals the most important stack of all: the one your programs run on. Quiz yourself on stacks in the **AI Learning app**.
A private, offline app to learn Artificial Intelligence, Machine Learning & Data Science at your own level: ~10,000 Q&A, illustrated guides, cheat sheets, quizzes and mock tests.
Sources
- Cormen, Leiserson, Rivest & Stein — Introduction to Algorithms, 4th ed. (MIT Press, 2022), Ch. 10.1: Stacks and Queues
- Sedgewick & Wayne — Algorithms, 4th ed. (Addison-Wesley, 2011), Ch. 1.3
- Dijkstra — An ALGOL 60 Translator for the X1 (the shunting-yard algorithm), Mathematisch Centrum, 1961
Frequently asked questions
More in Learn AI & Data Science
- Confusion Matrix Explained: TP, FP, FN, TN — and the Metrics They BuildEvery classification metric you've heard of — accuracy, precision, recall, F1 — is built from the same four numbers. Here's how to read them.
- Precision vs Recall: What They Mean and When to Optimize WhichTwo metrics, two different kinds of failure. The right one to optimize depends on which mistake costs you more.
- Cross-Validation Explained: How K-Fold Works and Why It Beats a Single SplitOne random test split can flatter or sabotage a model by pure luck. K-fold cross-validation replaces that lottery with an honest average.