Stack Parsing and Expression Evaluation
Whenever the most recently opened thing must be closed first, you are
looking at a stack. Brackets, nested arithmetic, [3[a2[c]]]-style
encodings, directory paths, undo history — all the same shape, because all
of them are last-in, first-out by definition.
This is a different use of a stack from Monotonic Stack. There, the stack holds a sorted set of candidates to answer next-greater-element questions. Here, the stack holds suspended context: work you must come back to once the current nested piece finishes.
What you’ll learn
Section titled “What you’ll learn”- Bracket matching, and the two failure modes people forget.
- Why postfix (RPN) needs no precedence rules at all.
- The save-and-restore template for nested encodings — push the outer context, start a fresh inner one, merge on close.
- How to accumulate multi-digit numbers while scanning character by character.
- Three real LeetCode problems solved in the browser: 20, 150, 394.
The cue
Section titled “The cue”Template 1 — matched pairs
Section titled “Template 1 — matched pairs”def is_valid(s):
pairs = {")": "(", "]": "[", "}": "{"} # closer -> its opener
stack = []
for ch in s:
if ch in pairs: # a closer
if not stack or stack.pop() != pairs[ch]:
return False # nothing open, or wrong type
else: # an opener
stack.append(ch)
return not stack # nothing may be left open
print(is_valid("([])")) # True
print(is_valid("([)]")) # False -- correctly nested matters, not just countsTemplate 2 — postfix evaluation, no precedence needed
Section titled “Template 2 — postfix evaluation, no precedence needed”Reverse Polish Notation puts operators after their operands, which removes precedence and parentheses from the problem entirely. Push operands; on an operator, pop two, combine, push the result.
def eval_rpn(tokens):
stack = []
for t in tokens:
if t in {"+", "-", "*", "/"}:
b = stack.pop() # SECOND operand pops FIRST
a = stack.pop()
if t == "+": stack.append(a + b)
elif t == "-": stack.append(a - b)
elif t == "*": stack.append(a * b)
else: stack.append(int(a / b)) # truncate toward zero
else:
stack.append(int(t))
return stack[0]
print(eval_rpn(["4", "13", "5", "/", "+"])) # 6 -> 4 + (13 // 5)Template 3 — save and restore for nested encodings
Section titled “Template 3 — save and restore for nested encodings”This is the pattern worth internalising, because it generalises to every “decode the nested thing” problem. Keep the current context in plain variables. On an opener, push the current context and reset. On a closer, pop and merge.
def decode_string(s):
stack = [] # suspended (text_before, repeat_count) pairs
current = "" # text being built at this nesting level
count = 0 # digits seen just before the next '['
for ch in s:
if ch.isdigit():
count = count * 10 + int(ch) # build multi-digit numbers
elif ch == "[":
stack.append((current, count)) # suspend the outer level
current, count = "", 0 # start a fresh inner level
elif ch == "]":
prev, k = stack.pop() # resume the outer level
current = prev + current * k # merge the finished inner one
else:
current += ch
return current
print(decode_string("3[a2[c]]")) # accaccaccThe count = count * 10 + int(ch) line matters: "10[a]" must repeat ten
times, not once then zero times. Treating each digit character as a
complete number is a real and common bug.
| Time | Space | |
|---|---|---|
| Bracket matching | worst case (all openers) | |
| RPN evaluation | ||
| Nested decoding |
Visual intuition
Section titled “Visual intuition”Stack depth is nesting depth — that equivalence is the whole of bracket matching:
Note the two failure modes the stack catches that a counter cannot: a closer arriving when nothing is open, and crossed nesting like ([)] where the counts balance but the order does not.
Watch the operand order on each operator: the SECOND operand pops first. Getting that backwards is invisible for + and *, and silently wrong for − and /.
The variant map
Section titled “The variant map”| Variant | What the stack holds | Canonical problem |
|---|---|---|
| Matched pairs | The unclosed openers | 20 Valid Parentheses |
| Count repairs needed | Just a counter plus one for unmatched closers | 921 Minimum Add to Make Parentheses Valid |
| Postfix evaluation | Operands awaiting an operator | 150 Evaluate RPN |
| Infix with precedence | Running value + the last operator, or two stacks | 227 · 224 |
| Nested decoding | Suspended (context, multiplier) | 394 Decode String |
| Adjacent cancellation | Characters, popping on a match | 1047 Remove All Adjacent Duplicates |
| Path resolution | Resolved directory names; .. pops | 71 Simplify Path |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 20 — Valid Parentheses · Easy
Section titled “LC 20 — Valid Parentheses · Easy”Problem. Given a string containing only (, ), {, }, [, ],
determine whether the brackets are correctly closed, in the correct order,
and each closer matches its opener type.
Constraints. 1 <= len(s) <= 10^4.
Examples. "()" gives True · "()[]{}" gives True ·
"(]" gives False · "([)]" gives False · "([])" gives True
Editorial — approach, complexity, follow-ups
Push openers; on a closer, the top of the stack must be its matching opener. Popping consumes the match. An empty stack at the end means every opener was closed.
Time . Space — worst case "(((((".
"([)]" is why this is a stack problem rather than a counting problem: the
counts of (, ), [, ] are all balanced, yet the nesting is wrong.
"]" tests the empty-stack guard, and "" should be True (vacuously
balanced) — though note LeetCode’s own constraints say len(s) >= 1, so
the empty string is included here to test your guard rather than because
the judge will send it.
Follow-ups you should expect: “Only one bracket type?” — an integer
counter suffices; it must never go negative and must end at zero. “Return
the index of the first invalid character?” — return i at the point of
failure. “Minimum insertions to make it valid (921)?” — count unmatched
closers as you scan plus whatever openers remain. “Longest valid substring
(32)?” — a genuinely harder problem: push indices and measure gaps, or
use DP.
LC 150 — Evaluate Reverse Polish Notation · Medium
Section titled “LC 150 — Evaluate Reverse Polish Notation · Medium”Problem. Evaluate an arithmetic expression in Reverse Polish Notation.
Valid operators are +, -, *, /. Division between two integers
truncates toward zero.
Constraints. 1 <= len(tokens) <= 10^4. The expression is always
valid, and division by zero never occurs.
Examples. ["2","1","+","3","*"] gives 9 ·
["4","13","5","/","+"] gives 6 ·
["10","6","9","3","+","-11","*","/","*","17","+","5","+"] gives 22
Editorial — approach, complexity, follow-ups
Postfix notation encodes precedence in the token order, so evaluation is a single pass with no lookahead: operands accumulate on the stack, and each operator consumes exactly the top two. Because the input is guaranteed valid, the stack holds exactly one value at the end.
Time . Space .
Also note that a token like "-11" is a negative operand, not a minus
operator — checking membership in the operator set (rather than
t.isdigit(), which is False for "-11") handles it correctly. Using
isdigit() here is a classic bug.
Follow-ups you should expect: “Evaluate infix instead?” — LC 227/224,
or convert infix to postfix with the shunting-yard algorithm and reuse this
code. “Prefix (Polish) notation?” — scan right to left with the pop order
swapped. “Add ^ or unary minus?” — extend the operator set; unary
operators pop only one value.
LC 394 — Decode String · Medium
Section titled “LC 394 — Decode String · Medium”Problem. Given an encoded string where k[encoded] means the bracketed
substring repeats exactly k times, return the decoded string. k is
guaranteed to be a positive integer, and the input is always valid.
Constraints. 1 <= len(s) <= 30, 1 <= k <= 300. Brackets may nest.
Examples. "3[a]2[bc]" gives "aaabcbc" ·
"3[a2[c]]" gives "accaccacc" ·
"2[abc]3[cd]ef" gives "abcabccdcdcdef"
Editorial — approach, complexity, follow-ups
The insight is to keep the current nesting level in ordinary variables and
push only the suspended outer levels. On [ you are descending, so save
what you had and start clean. On ] the inner level is complete, so pop the
parent and splice the repeated result into it.
Time — you cannot beat that, since you must produce every character. Space .
Two details that break naive attempts:
- Multi-digit counts.
"10[a]"must give tenas.count = count * 10 + int(ch)accumulates digits; treating each digit as its own number gives"a"repeated once then zero times. - Text before a bracket.
"abc3[cd]xyz"shows thatcurrentmay already hold characters when[arrives, which is exactly why the pushed tuple storescurrentalongside the count, and why the merge isprev + current * krather than justcurrent * k.
The recursive solution is equally valid and arguably more readable: parse a number, recurse on the bracketed segment, repeat and continue. The stack version is the iterative form of that same recursion, which is why “now do it without recursion” is such a common follow-up.
Follow-ups you should expect: “Do it recursively” (or iteratively, if
you started recursive) — be ready to convert either way. “What if k
could be zero?” — the merge still works and drops the segment. “Encode
rather than decode?” — a different, harder problem (run-length
compression with nesting). “What if the input might be malformed?” — add
the LC 20 guards: check the stack is non-empty before popping and empty at
the end.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 20Valid ParentheseseasyThe base template; guard the empty stack and the leftovers
- 1047Remove All Adjacent Duplicates In StringeasyPush, and pop instead when the incoming char equals the top
- 71Simplify PathmediumSplit on `/`; `..` pops, `.` and empty segments are skipped
- 150Evaluate Reverse Polish NotationmediumPop order, and truncation toward zero
- 227Basic Calculator IImediumApply `*` and `/` eagerly, defer `+` and `-` to a final sum
- 394Decode StringmediumSave-and-restore; multi-digit counts
- 678Valid Parenthesis Stringmedium
- 921Minimum Add to Make Parentheses ValidmediumA counter is enough -- count unmatched closers plus leftover openers
- 32Longest Valid ParentheseshardPush **indices**, not characters, and measure the gap to the last unmatched one
- 224Basic CalculatorhardParentheses: push the running total **and** the sign, restore on `)`
Dry run
Section titled “Dry run”LC 394 Decode String on "3[a2[c]]" — the nested case that separates a working
solution from one that only handles a single level.
ch | action | num | cur | stack (bottom → top) |
|---|---|---|---|---|
3 | accumulate digit | 3 | "" | — |
[ | push (cur, num), reset both | 0 | "" | [("", 3)] |
a | append to cur | 0 | "a" | [("", 3)] |
2 | accumulate digit | 2 | "a" | [("", 3)] |
[ | push (cur, num), reset both | 0 | "" | [("", 3), ("a", 2)] |
c | append to cur | 0 | "c" | [("", 3), ("a", 2)] |
] | pop ("a", 2); cur = "a" + "c"*2 | 0 | "acc" | [("", 3)] |
] | pop ("", 3); cur = "" + "acc"*3 | 0 | "accaccacc" | [] |
Answer "accaccacc".
Three details that are the whole problem:
- The stack stores a pair. Pushing only the multiplier loses the text built
before the bracket, and
"a2[c]"comes back as"cc"instead of"acc". - Digits must accumulate across characters.
num = num * 10 + int(ch), notnum = int(ch)— otherwise"12[a]"repeats twice, not twelve times. - Both
numandcurreset on[. They belong to the level being entered, and forgetting to reset leaks state downward into the nested level.
Compare with LC 224 Basic Calculator on "1+(2-(3+4))", where the stack holds a
running result and a sign instead of a string and a count. Same shape, different
payload — that shape is the pattern.
Complexity
Section titled “Complexity”| Problem shape | Time | Space |
|---|---|---|
| Bracket matching (LC 20) | — the stack, at worst all openers | |
| Expression evaluation (LC 150, 224, 227) | ||
| Decode String (LC 394) |
Two things worth saying out loud:
- Space is genuinely , not . A fully nested input like
"((((((..."puts every character on the stack. If asked to reduce it: for balance checking only a counter suffices, but a counter cannot detect crossed nesting like"([)]", so it answers a weaker question. - Decode String is measured against its output, not its input.
"10[10[a]]"is nine characters and expands to a hundred, so quoting in the input length is wrong — and being precise about which you mean is exactly the kind of care these rounds are looking for.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Could you use a counter instead of a stack?” | Whether you know the boundary | Only with a single bracket type; multiple types need to know what was opened, which "([)]" demonstrates |
“Why int(a / b) and not a // b?” | Reading the spec | The problem truncates toward zero; Python’s // floors toward negative infinity, so they differ on negative results |
| “Now do it iteratively / recursively” | Flexibility | A stack is the explicit form of the call stack — the save-and-restore template is the mechanical conversion |
| “What’s the space complexity?” | Precision | worst case for matching (all openers); for decoding, which can exceed the input |
| “Handle malformed input” | Defensiveness | Guard every pop() with a non-empty check, and verify the stack is empty at the end |
| “Add precedence / parentheses to the calculator” | Depth | Apply higher-precedence ops eagerly on a stack of terms; for parentheses, push and restore the running total and sign |
Edge-case checklist
Section titled “Edge-case checklist”- Closer with an empty stack —
"]"must returnFalse, not raiseIndexError. - Unclosed opener at the end —
"("; the final emptiness check is the only thing that catches it. - Correct counts, wrong nesting —
"([)]"; the reason a counter is insufficient. - Empty input — decide what it means and be consistent.
- Negative operands in RPN —
"-11"is a number;isdigit()returnsFalsefor it, so test membership in the operator set instead. - Negative division in RPN —
["7","-3","/"]gives-2, not-3. - Multi-digit repeat counts —
"10[a]"gives ten characters. - Text before and after a bracket group —
"abc3[cd]xyz"; the merge must preserveprev. - Deep nesting — a recursive solution can hit Python’s recursion limit; the iterative stack version cannot.
Self-check
Section titled “Self-check”-
A counter of open brackets is simpler than a stack. Why is it not sufficient for LC 20?
A counter answers the weaker question 'do the counts balance'. The stack answers 'is the nesting well-formed', because its top is always the bracket that must close next. With one bracket type a counter genuinely is enough — worth saying if asked.
pch.quizShowAnswer
B — A counter cannot detect crossed nesting like ([)], where the counts balance but the order is invalid — A counter answers the weaker question 'do the counts balance'. The stack answers 'is the nesting well-formed', because its top is always the bracket that must close next. With one bracket type a counter genuinely is enough — worth saying if asked.
-
In Decode String, what must be pushed onto the stack at each `[`?
Pushing only the count loses the prefix: 'a2[c]' returns 'cc' rather than 'acc'. Pushing only the string loses the multiplier. The pair is what lets the pop reconstruct prefix + repeated middle.
pch.quizShowAnswer
B — Both the string built so far and the repeat count, as a pair — Pushing only the count loses the prefix: 'a2[c]' returns 'cc' rather than 'acc'. Pushing only the string loses the multiplier. The pair is what lets the pop reconstruct prefix + repeated middle.
-
Why is `num = num * 10 + int(ch)` rather than `num = int(ch)`?
A single-digit test suite hides this completely. It is the most common reason a Decode String solution passes the samples and fails the hidden tests.
pch.quizShowAnswer
B — Because multi-digit counts arrive one character at a time, so '12[a]' must repeat twelve times, not two — A single-digit test suite hides this completely. It is the most common reason a Decode String solution passes the samples and fails the hidden tests.
-
In Evaluate Reverse Polish Notation, which operand pops first?
Stacks are LIFO, so the operand pushed last comes off first, and that is the right-hand operand. The mistake is invisible for + and *, and silently wrong for − and /.
pch.quizShowAnswer
B — The second operand, b — so the expression is a op b, not b op a — Stacks are LIFO, so the operand pushed last comes off first, and that is the right-hand operand. The mistake is invisible for + and *, and silently wrong for − and /.
-
What is the space complexity of Decode String, and in terms of what?
Quoting O(n) without saying which n is wrong here. Being explicit that the bound is in the output length, and giving the expanding example, is the kind of precision these rounds score.
pch.quizShowAnswer
C — O(output length) — '10[10[a]]' is nine characters and expands to a hundred — Quoting O(n) without saying which n is wrong here. Being explicit that the bound is in the output length, and giving the expanding example, is the kind of precision these rounds score.
Recall card
Section titled “Recall card”- Cue — nested structure in a string: brackets, parentheses, an expression, a path to simplify, a string to decode.
- Invariant — the stack holds the enclosing context, one entry per level currently open. Its depth is the nesting depth.
- Template — scan left to right; on an opener push the current context and reset it; on a closer pop and combine the popped context with what was built inside.
- What to push — a pair, almost always: (text so far, repeat count) for Decode String; (running result, sign) for Basic Calculator. Pushing one half is the standard bug.
- Complexity — time, space. For Decode String, — say which you mean.
- Remember — accumulate multi-digit numbers with
num*10 + d; reset the level’s state on the opener; in RPN the second operand pops first.
- A stack is the right tool whenever the most recent open thing must be resolved first — brackets, nesting, suspended context.
- Matched pairs: push openers, match on closers, and check both failure modes (empty stack mid-scan, leftovers at the end).
- Postfix needs no precedence handling; just watch the pop order and truncate division toward zero.
- Nested decoding uses save-and-restore: push
(context, multiplier)on the way in, pop and merge on the way out. Accumulate multi-digit numbers withcount * 10 + digit. - This is stack-as-suspended-context; stack-as-sorted-candidates is Monotonic Stack.
- Any recursive descent over nested input can be rewritten with this template — which is exactly what “now do it iteratively” is asking for.
Next: Frequency and Anagram Counting — the Counter-based patterns
behind anagram grouping and character-multiset comparison.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading