Stack — when you need the most recent unresolved thing
📖 Walk me through it — plain English
A stack is a list where you only ever touch one end. You push (add) onto the top, and you pop (remove) from that same top. So the item you get back is always the one you added most recently — that rule is called LIFO, "last in, first out." Nothing buried underneath is reachable until you've removed everything above it.
Picture a stack of dinner plates. You set clean plates on top, and when you need one you take from the top — you never yank a plate out of the middle. The plate you grab is the one you put down last. That "deal with the most recent thing first" behavior is exactly what bracket-matching needs.
Here's the problem this lesson solves: given a string of brackets, decide if it's balanced — every opener has a matching closer, properly nested. The trick: when you see a closing bracket, the opener it must pair with is whichever opener is still unmatched and was opened most recently — that's the one sitting on top of the stack. The code keeps a map pair that, given a closer like '}', tells you the opener it needs, '{'. For each character: if it's an opener, push it; if it's a closer, pop the top and check it equals the opener we expected. At the very end, a balanced string leaves the stack empty.
Two ways it can fail, both caught by the same code. If you pop and the opener doesn't match (e.g. input [ ) pops a [ when pair[')'] wanted (), return False. If you hit a closer with nothing on the stack (the not st check), there's no opener to pair with — also False. And if openers are left over at the end (stack not empty), they were never closed, so not st is False too.
Why it's fast: you look at each character exactly once, and a push or a pop is constant work, so the whole thing is O(n) time for a string of length n. Extra space is O(n) in the worst case — a string that's all openers (like (((() stacks every one before any get popped. The stack works here because "match the most recent unresolved opener" is precisely LIFO, and the stack hands you that most-recent item for free.
A stack is last-in, first-out: the only item you can reach is the one you added most recently. That's exactly the shape you want whenever the next thing to resolve is always the most recent unresolved one. Matching brackets is the clean example — when you hit a closing bracket, it has to pair with the most recent still-open one, which is sitting right on top. Push openers, pop on a match; if the top doesn't match, the string is invalid.
The vocabulary, defined once
Every word in this lesson points at a precise, simple idea. Pin them down now and the rest reads itself.
- Stack — an ordered collection where insertions and removals happen at one end only, called the top. In most languages it is just a plain dynamic array (Python
list, JS array) used through a restricted set of operations; the discipline, not the data type, is what makes it a stack. - LIFO — "last in, first out": the most recently added item is the first one removed. Contrast with a queue, which is FIFO ("first in, first out", like a line at a checkout).
- Push — add an item onto the top. In Python that is
st.append(x); in JSst.push(x). Cost: O(1) amortized. - Pop — remove and return the top item. Python
st.pop(), JSst.pop(). Cost: O(1). Popping an empty stack is an error you must guard against. - Peek (a.k.a. top) — look at the top item without removing it. Python
st[-1], JSst[st.length - 1]. Use it when you need to inspect before deciding whether to pop. - Top — the position (and the item) at the open end of the stack; "peek" is the act of reading it.
- Matching / balanced parentheses — a string of bracket characters in which every opener (
( [ {) has a corresponding closer () ] }) of the same kind, and pairs are properly nested (you never close an outer bracket while an inner one is still open).([])is balanced;([)]is not. - Expression evaluation — computing the value of something like
3 + 4 * 2or its parenthesized / postfix forms. Stacks hold pending operands and operators until precedence says it's time to apply them. - Monotonic stack (previewed below) — a stack you deliberately keep sorted in one direction (always increasing or always decreasing) by popping anything that would break the order before you push. It is the engine behind "next greater element" and similar problems.
The call stack — the stack you already use
You have been relying on a stack since your very first function call. When your program calls a function, the runtime pushes a stack frame onto the call stack — a record holding that call's local variables and the spot to return to when it finishes. Call another function from inside, and a new frame is pushed on top. When a function returns, its frame is popped, and control resumes in the frame now exposed at the top. This is pure LIFO: the most recently entered function is always the next to finish. That is also why deep, unbounded recursion crashes with a "stack overflow" — you pushed more frames than the call stack can hold. Understanding this makes the next idea click: any recursive algorithm can be rewritten with an explicit stack you manage by hand, because all you are doing is reproducing the call stack's LIFO bookkeeping yourself.
- Balanced brackets / parser-y
- "Next greater element"
- Iterative tree traversal
- Undo / linear backtracking
- Monotonic (increasing/decreasing)
- Stack of (value, count) — RLE-style
- Two stacks for queue / max-stack
def is_valid(s):
pair = {')':'(', ']':'[', '}':'{'}
st = []
for ch in s:
if ch in pair:
if not st or st.pop() != pair[ch]:
return False
else:
st.append(ch)
return not st
function isValid(s: string): boolean {
const pair: Record<string,string> = {")":"(", "]":"[", "}":"{"};
const st: string[] = [];
for (const ch of s) {
if (ch in pair) { if (st.pop() !== pair[ch]) return false; }
else st.push(ch);
}
return st.length === 0;
}
Reading the template line by line
The whole solution is nine lines, and each one earns its place. pair is the lookup that turns the question "what opener does this closer need?" into a single O(1) dictionary read — keying on closers means ch in pair doubles as the test "is this character a closer?". st is the stack of openers we have seen but not yet closed. The loop visits each character once. If ch is a closer, we demand two things in one expression: the stack is non-empty (not st guards against a closer with no opener waiting) and the popped top equals the expected opener; failing either returns False. Otherwise ch is an opener, so we push it to wait for its partner. The final not st is the cleanup verdict: True only when nothing is left unmatched. Python's short-circuit or matters here — not st is checked before st.pop(), so we never pop an empty stack.
The canonical stack problem — write it and run it live:
The family of stack problems
Valid Parentheses is one member of a family. The shared DNA: at each step the thing you must act on is the most recent unresolved item, and a stack hands you that item in O(1). Learn to spot which member you are looking at.
- Matching / nesting — balanced brackets, valid HTML/XML tags, removing matched pairs. Push openers, pop on the corresponding closer. Same skeleton as the template above.
- Undo / linear backtracking — a text editor's undo, simplifying a Unix path (
cd ..pops the last directory off the stack), processing"..."with backspaces. The most recent action is the first to reverse — LIFO again. - DFS / iterative tree & graph traversal — replace the recursive call stack with your own. Push the start node; loop while the stack is non-empty: pop a node, process it, push its unvisited neighbors. The explicit stack reproduces recursion's depth-first order without risking a stack-overflow on deep inputs.
- Expression evaluation — evaluate
(2 + 3) * 4or its postfix form2 3 + 4 *. Keep one stack of operands (and, for infix, one of operators); when precedence or a closing paren says "apply now," pop the operands, apply the operator, push the result back. The shunting-yard algorithm is this idea fully fleshed out. - Next-greater / next-smaller (monotonic stack) — "for each element, find the next one to its right that is larger." Walk the array keeping a stack of indices whose answers are still pending, in decreasing value order. Each new value pops every smaller value beneath it — that new value is their next-greater answer — then pushes itself. Each element is pushed and popped at most once, so the whole sweep is O(n) even though it feels like a nested search. Stock-span, daily-temperatures, and largest-rectangle-in-histogram all run on this.
Recognition signals & pitfalls
Reach for a stack the moment a problem's phrasing carries one of these tells: "matching" or "nested" pairs; "most recent," "last," or "innermost"; "undo" or "previous unmatched"; "next greater/smaller/warmer/closer" element; or "evaluate an expression." If you find yourself wanting to peek backward at the most recent thing you have not finished with, that is a stack.
Two classic bugs, both about the boundaries:
- Popping an empty stack. A lone closer (
")") or an extra one means there is no opener waiting. Always test the stack is non-empty before you pop — that is the job ofnot st or .... In Python,[].pop()raisesIndexError; in JS,[].pop()silently returnsundefined, which can pass a sloppy comparison and hide the bug. - Leftover items at the end. If you only check matches during the loop and forget the final emptiness test, an input of all openers (
"(((") reports valid because nothing ever mismatched. The closing verdictreturn not st(orst.length === 0) is not optional — leftover openers were never closed. - Wrong-type match.
"[)"must fail: it has equal counts but the pair is wrong. Comparing the popped opener againstpair[ch](not just "is the stack non-empty") is what catches this.
Takeaway: a stack is a one-ended list with O(1) push/pop/peek that gives you the most recently added item for free — that's LIFO. Use it whenever the next thing to resolve is the most recent unresolved one: matching brackets, undo, iterative DFS, expression evaluation, and next-greater (monotonic) sweeps. Guard the two edges every time — never pop an empty stack, and always check the stack is empty at the end.
Go deeper (optional): for expression parsing, look up Dijkstra's shunting-yard algorithm and Reverse Polish (postfix) notation; for the monotonic-stack family, the largest rectangle in a histogram problem is the canonical hard case.