~/data-structures/stacks
Stacks
Last in, first out. A stack hands you the most recent unfinished thing in O(1): the tool for brackets, undo and postfix.
A list you only touch at one end: push on top, pop from the top, peek at the top. The last thing in is the first out.
The most recent unfinished thing must be dealt with first: an open bracket, an operand waiting for its operator, a state to go back to.
O(1) per push, pop or peek
O(n)
You’ll recognise it when
- Things come in pairs that nest: brackets, HTML tags,
beginandend. Each closer must match the newest opener that is still open. - You keep needing the most recent unmatched thing: the last open bracket, the last folder you entered, the last item still waiting.
- There’s an undo: actions are reversed newest first.
- You’re evaluating an expression. In postfix,
3 4 + 2 *, each operand waits until its operator arrives. - A recursive solution is right, but the recursion is too deep for Python. An explicit stack does the same work in a loop.
It’s easy to confuse with the monotonic stack, a stack with one extra rule: pop everything smaller before you push. Reach for that one when each element needs its next bigger or smaller neighbour.
The idea
Think of a pile of plates. You put plates on top and take them from the top. The plate you can reach is always the one you put down last. That’s last in, first out, or LIFO.
A stack has three operations, and each takes O(1): push puts a value on top, pop removes the top and returns it, and peek reads the top without removing it. In Python a plain list is the stack: stack.append(x), stack.pop() and stack[-1]. In C++ use a vector (push_back, pop_back, back), and in Java an ArrayDeque (push, pop, peek).
The point of a stack is that it gives things back in the order you’ll need them. When the newest unfinished item must be handled first, the stack keeps it on top for you.
Its first-in, first-out partner is the queue, like a line at a till: add at the back, take from the front. In Python use collections.deque with append and popleft. A deque (double-ended queue) does both jobs: it pushes and pops at either end in O(1).
How it works
We’ll check whether the brackets in a string are balanced: every opener is closed by the same kind, in the right order.
Scroll through the steps and the graphic follows along. The string runs along the top, and the stack stands on the right with its top at the top. You can also press play, or edit the input: try ([)], a lone ), or ((.
- Start with an empty
stack. Read the string one characterchat a time. - An opener can’t be checked yet, because its closer hasn’t arrived. Push it: it waits on the stack.
- A closer can only close the newest opener that is still open. That’s the
topof the stack, so comparechwith it. )meets(: they match, so pop. The opener underneath is back on top.- Nesting takes care of itself. When
]arrives, both inner pairs have been popped, so[is on top again and matches. - At the end the stack must be empty. Anything left over is an opener that was never closed.
Why it’s correct: the stack always holds the openers that are still open, oldest at the bottom. Proper nesting means the next closer must close the newest of them, so checking the top is enough. A string can fail in exactly three ways, and each is one line of the code: a closer arrives when the stack is empty, a closer doesn’t match the top, or openers are left over at the end.
PAIRS = {")": "(", "]": "[", "}": "{"} # each closer and the opener it needs
def is_balanced(s):
"""True if every bracket in s is closed by the same kind, in the right order."""
stack = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif ch in PAIRS:
if not stack or stack[-1] != PAIRS[ch]:
return False
stack.pop()
return not stack#include <string>
#include <vector>
using namespace std;
// The opener that a closing bracket needs.
char openerFor(char close) {
if (close == ')') return '(';
if (close == ']') return '[';
return '{';
}
// True if every bracket in s is closed by the same kind, in the right order.
bool isBalanced(const string& s) {
vector<char> stack;
for (char ch : s) {
if (ch == '(' || ch == '[' || ch == '{') {
stack.push_back(ch);
} else if (ch == ')' || ch == ']' || ch == '}') {
if (stack.empty() || stack.back() != openerFor(ch))
return false;
stack.pop_back();
}
}
return stack.empty();
}import java.util.ArrayDeque;
import java.util.Deque;
class Brackets {
// The opener that a closing bracket needs.
static char openerFor(char close) {
if (close == ')') return '(';
if (close == ']') return '[';
return '{';
}
// True if every bracket in s is closed by the same kind, in the right order.
static boolean isBalanced(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char ch : s.toCharArray()) {
if (ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
} else if (ch == ')' || ch == ']' || ch == '}') {
if (stack.isEmpty() || stack.peek() != openerFor(ch))
return false;
stack.pop();
}
}
return stack.isEmpty();
}
}Why it’s O(n)
Each character is read once. An opener costs one push. A closer costs one peek and at most one pop. All of them are O(1), so the whole scan is O(n).
| Operation | Python | C++ vector |
Java ArrayDeque |
Time |
|---|---|---|---|---|
| push | s.append(x) |
s.push_back(x) |
s.push(x) |
O(1) amortised |
| pop | s.pop() |
s.pop_back() |
s.pop() |
O(1) |
| peek | s[-1] |
s.back() |
s.peek() |
O(1) |
| empty? | not s |
s.empty() |
s.isEmpty() |
O(1) |
“Amortised” means a push sometimes has to grow the list and copy it, but averaged over all pushes each one is O(1).
Space is O(n) in the worst case: a string of only openers, like ((((, pushes every character.
Common mistakes
Peeking at an empty stack
A closer with nothing open is the classic case. stack[-1] on an empty list raises IndexError, and in C++ back() on an empty vector is undefined behaviour. Check first.
if stack[-1] != PAIRS[ch]: # ✗ crashes on ")"
if not stack or stack[-1] != PAIRS[ch]: # ✓ empty means nothing to match
Forgetting the leftovers
Every closer in (( matched, because there are none. If the loop ends with return True, unclosed openers slip through.
return True # ✗ "((" passes
return not stack # ✓ anything left was never closed
Popping operands in the wrong order
In postfix, 8 2 - means 8 - 2. The 2 was pushed last, so it comes off first: the first value you pop is the right operand.
stack.append(stack.pop() - stack.pop()) # ✗ computes 2 - 8
b, a = stack.pop(), stack.pop()
stack.append(a - b) # ✓ 8 - 2
Using a list as a queue
list.pop(0) shifts every remaining element left, so it’s O(n), and a loop of them is O(n²). When you need first in, first out, use a deque.
first = queue.pop(0) # ✗ O(n) on a list
first = queue.popleft() # ✓ O(1) on a collections.deque
Variations
- Postfix evaluation. Push numbers. On an operator, pop two values, apply it (the first one popped goes on the right), and push the result. One value is left at the end.
- Minimum in O(1). Push the pair
(x, min(x, current minimum))instead ofx. The minimum of the whole stack is always on top, and popping brings back the previous minimum for free. - Undo log. For each action, push what you need to reverse it: how many letters were typed, or the text that was deleted. Undo pops the newest entry and applies it.
- Replacing recursion. Push the work a recursive call would have done, and loop while the stack isn’t empty. This is how depth-first search avoids Python’s recursion limit of about 1,000.
- A queue from two stacks. Push onto an “in” stack. When the “out” stack is empty, pour everything across: pouring reverses the order, so the oldest item lands on top. Each item moves at most once, so every operation is O(1) amortised.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
You must check that the tags in an HTML snippet like
<b><i>hi</i></b>are properly nested. Which structure fits best?A closing tag must close the newest tag still open, which is exactly the top of a stack. A queue hands you the oldest open tag instead. Counting accepts
<b><i></b></i>, where the counts match but the tags cross. -
2
What does this print?
PAIRS = {")": "(", "]": "[", "}": "{"}def ok(s):stack = []for ch in s:if ch in "([{":stack.append(ch)elif not stack or stack.pop() != PAIRS[ch]:return Falsereturn not stackprint(ok("{()[]}"), ok("([)]"), ok("(()"), ok(""))In
([)]the)meets[on top, so it fails even though the counts match.(()ends with one(still on the stack, andnot stackcatches it. The empty string pushes nothing, so the stack is empty and it’s balanced. -
3
What does this print?
stack = []for tok in "6 2 / 3 -".split():if tok in "+-*/":b, a = stack.pop(), stack.pop()stack.append(a + b if tok == "+" else a - b if tok == "-" else a * b if tok == "*" else a // b)else:stack.append(int(tok))print(stack)6 2 /popsb = 2, thena = 6, and pushes6 // 2 = 3. Then3 3 -gives 0. The first value popped is the right operand: popping the other way round computes2 // 6 = 0and then3 - 0 = 3.//keeps the result an integer, so it isn’t0.0. -
4
This bracket checker gives a wrong answer on some inputs. Which one?
def ok(s):stack = []for ch in s:if ch in "([{":stack.append(ch)elif not stack or stack.pop() != {")": "(", "]": "[", "}": "{"}[ch]:return Falsereturn TrueEvery closer in
(()matches, so the loop never returns False, and the unclosed(is ignored. End withreturn not stack. The empty-stack case is handled:not stackshort-circuits beforepop()runs. -
5
A BFS uses a Python list as its queue:
queue.append(x)to add andqueue.pop(0)to take the next item. With n items passing through, what’s the cost of the queue operations?A list is fast at its end, not its front.
pop(0)moves every other element one slot left, so it’s O(n) per call.collections.dequewithpopleft()is O(1).pop()with no argument is the O(1) stack pop.
Practice problems
Solve these right here, in Python, C++ or Java. Tests run as you go.