Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Push every opening bracket onto a stack; on each closing bracket, the top of the stack must be its matching opener — pop it, else the string is invalid. The string is valid only if every closer matched and the stack is empty at the end. O(n) time, O(n) space. The stack models 'most recently opened must close first', which is exactly LIFO.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you reach for a stack on a nesting problem and handle all three failure modes — wrong type, closer with nothing open, and leftover openers at the end.
Where candidates slip
Using a counter instead of a stack (accepts mismatched types like ([)]), forgetting the empty-stack check before popping, or not verifying the stack is empty at the end.
How to stand out
Name the LIFO insight, enumerate the three ways a string can be invalid up front, and map closers to openers so the inner check is one comparison.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewRead char cIs c an opener?YesNo (closer)Explore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Repeated String Replacement (Baseline)
1Hint 1Explore hintHide hint
A valid string must contain at least one innermost pair like () or [] or {}. What if you keep deleting those?
2Hint 2Explore hintHide hint
Repeatedly remove every occurrence of "()", "[]", "{}" until nothing changes.
3Hint 3Explore hintHide hint
If the string becomes empty it was valid. It works but each removal pass is O(n) and you may need O(n) passes → O(n²).
A · Core idea
Repeatedly delete adjacent matched pairs ((), [], {}). A fully valid string collapses to empty; anything left over means an unmatched or mis-nested bracket.
Why mention it. It illustrates the nesting intuition concretely — innermost pairs resolve first — and is a legitimate (if slow) solution. Why we don't ship it. String mutation plus repeated full scans is O(n²) and allocates new strings each pass. The stack achieves the same logic in one linear pass.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "([])"pass 1
remove inner '[]' → "()"
"()"trueC · Code with teaching comments
Reference implementation previewpublic boolean isValid(String s) { int prevLength = -1; while (s.length() != prevLength) { prevLength = s.length(); s = s.replace("()", "").replace("[]", "").replace("{}", "");Explore the full codeHide the full code
D · Why it works and complexityO(n²) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Peeling innermost pairs is exactly what a stack does implicitly: when a closer meets its opener on top, that's an innermost pair being removed — but the stack removes it in O(1) without rescanning.
ComplexityO(n²) time, O(n) space — Each pass scans the whole string O(n); up to n/2 passes are needed as pairs peel off one layer at a time → O(n²).
E · Edge cases and pitfalls2O(n²) time from repeated full-string scans and allocations.Explore edge casesHide edge cases
Pitfalls
- O(n²) time from repeated full-string scans and allocations.
- Assuming one replacement pass suffices — nested layers need many passes.
Approach 2 of 2 · Optimal
Stack — One Pass (Optimal)
1Hint 1Explore hintHide hint
When you see a closing bracket, which open bracket does it have to match — the first one you saw or the most recent unmatched one?
2Hint 2Explore hintHide hint
Push each opener. On a closer, the correct partner is whatever's on top of the stack; pop and compare.
3Hint 3Explore hintHide hint
Reject early if a closer finds an empty stack or a non-matching top. At the end, the stack must be empty.
A · Core idea
Scan the string left to right with a stack of unmatched opening brackets.
Why a stack. Nesting imposes a last-in-first-out discipline: the bracket that opened most recently is the one that must close next. A stack is the data structure whose push/pop are exactly that discipline, so it mirrors the problem with no bookkeeping.
The two cases per character. If the character is an opener ((, [, {), push it — it's now the innermost context awaiting closure. If it's a closer, it must resolve against the most recent opener: peek/pop the top and check it's the matching opener ()→(, ]→[, }→{). Mapping closer → opener lets this be a single equality check.
The three ways to fail. (1) A closer arrives with an empty stack — there's no opener to match, invalid. (2) The popped top is the wrong type — e.g. ] on top of (, which also rejects mis-nesting like ([)]. (3) The scan finishes but the stack is non-empty — some openers were never closed ((((). Only if none of these happen is the string valid.
Why a counter isn't enough. With a single bracket type you could just track a balance counter. But three types interact through order: ([)] has matching counts yet is invalid. The stack records which type is open and in what nesting order, so it catches type/order errors a counter cannot.
Early termination. We can return false the instant any check fails, so invalid strings often exit before reading everything — though worst-case (all valid or failing at the end) is still O(n).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "([])"Array state
c='('
push '('
trueC · Code with teaching comments
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The stack turns 'remember the context I must return to' into O(1) push/pop. That framing — push when you enter a nested scope, pop when you leave it — recurs in expression evaluation, the call stack itself, HTML/XML tag validation, and Min Stack. Whenever a problem says 'properly nested', think stack first.
ComplexityO(n) time, O(n) space — Each character is pushed and popped at most once → O(n) time. The stack can hold up to n openers (e.g. "(((((") → O(n) space.
E · Edge cases and pitfalls8s = "]" — Closer with empty stack → return false immediately.Explore edge casesHide edge cases
Edge cases handled
s = "]"Closer with empty stack → return false immediately.s = "("Opener pushed, loop ends, stack non-empty → false.s = "([)]"')' pops '[' → type mismatch → false. Stack catches the mis-nesting a counter would miss.s = "()[]{}"Each closer matches and pops; stack empty at the end → true.
Pitfalls
- Popping before checking the stack is non-empty — exception on a leading closer.
- Forgetting the final emptiness check — accepts unclosed openers like '((('.
- Using a counter instead of a stack — accepts mismatched types ([)].
- Mapping opener→closer and then writing awkward reverse lookups.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic boolean isValid(String s) { Deque<Character> stack = new ArrayDeque<>(); Map<Character, Character> pairs = Map.of(')', '(', ']', '[', '}', '{'); for (char c : s.toCharArray()) { if (pairs.containsKey(c)) { // closerExplore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Counter instead of stack
Counting open vs close ignores type and nesting order, so '([)]' (equal counts, wrong nesting) is wrongly accepted. You must track which type is open, which a stack does.
Explore the code comparisonHide the code comparison
Counter instead of stack
Counting open vs close ignores type and nesting order, so '([)]' (equal counts, wrong nesting) is wrongly accepted. You must track which type is open, which a stack does.
Don't write this
bal = 0
for c in s:
if c in '([{': bal += 1
else: bal -= 1
if bal < 0: return False
return bal == 0Write this instead
stack = []
pairs = {')': '(', ']': '[', '}': '{'}
for c in s:
if c in pairs:
if not stack or stack.pop() != pairs[c]:
return False
else:
stack.append(c)
return not stack2Popping an empty stack
If a closing bracket appears with no open bracket pending, popping throws (Java) or errors (Python). Always test emptiness before popping.
Explore the code comparisonHide the code comparison
Popping an empty stack
If a closing bracket appears with no open bracket pending, popping throws (Java) or errors (Python). Always test emptiness before popping.
Don't write this
for (char c : s.toCharArray()) {
if (pairs.containsKey(c)) {
if (stack.pop() != pairs.get(c)) return false; // pop() on empty throws
} else {
stack.push(c);
}
}Write this instead
for (char c : s.toCharArray()) {
if (pairs.containsKey(c)) {
if (stack.isEmpty() || stack.pop() != pairs.get(c)) return false;
} else {
stack.push(c);
}
}Revise
Carry the pattern into the interview
Pattern to remember
LIFO matching with a stack
Code pattern to memorize
stack = []
pairs = {')':'(', ']':'[', '}':'{'}
for c in s:
if c in pairs: # closer
if not stack or stack.pop() != pairs[c]: return False
else: # opener
stack.append(c)
return not stackRules to remember
- 1Push openers, and on a closer check the stack top is its exact match before popping.
- 2A closer with an empty stack → invalid immediately (nothing to close).
- 3At the end, the stack must be empty — leftover openers mean unclosed brackets.
- 4Map closer→opener (not opener→closer) so the closer branch is a single
pop() == map[c]check. - 5Three bracket types means matching by type, not just count — a counter alone passes
([)], which is wrong.
Use this pattern when
- Balanced/nested structure validation: brackets, tags, scopes
- 'Most recent unmatched thing must resolve first' (LIFO) situations
- Parsing where you need to remember a context to return to
Choose another pattern when
- Single bracket type and you only need a yes/no count → an integer counter is O(1) space
- Order doesn't matter, only totals → counting, not a stack
- You need all matchings/positions, not just validity → augment the stack to store indices
Interview takeaway
A stack is the natural model for nesting: the last thing opened is the first that must close. Recognising 'nested / balanced' as a LIFO problem is the reusable insight behind expression parsing, the min-stack, and RPN evaluation.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternStack for balanced/nested structure validation. Reach for it whenever 'most recently opened must close first' (LIFO) applies — brackets, tags, scopes. Same family as Min Stack, Evaluate RPN, and expression parsing.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Generate ParenthesesProduce all valid combinations via backtracking with open/close counts.
- Longest Valid ParenthesesComing soonStack of indices (or DP) to measure the longest balanced run.
- Minimum Remove to Make Valid ParenthesesComing soonStack of indices; remove the unmatched positions at the end.
- Minimum Add to Make Parentheses ValidComing soonSingle type → a counter of unmatched opens and closes suffices.