Solving tips
- Recognize the nesting property: the only closer that can legally appear matches the most recently opened bracket, which is exactly a stack's top.
- Scan once: push openers, and on a closer pop and compare types, failing if the stack is empty or the popped opener doesn't match.
- Two must-do checks: guard against popping an empty stack (input like ']'), and return not stack at the end so unclosed openers like '((' fail.
- Counting opens vs closes is not enough (it accepts '([)]'); comparing types via the stack is what rejects interleaving. Target O(n) time and O(n) space.
Problem
You get a string made up only of the six bracket characters: (, ), {, }, [, ]. Decide whether the string is balanced: every opening bracket must be closed by a closing bracket of the same type, and brackets must close in the reverse order they were opened (the most recently opened bracket is always the first one closed). An empty stack of unfinished brackets at the end means the string is valid.
Return True if the string is balanced, False otherwise.
Examples
"()[]{}" β True β three independent pairs, each opened and immediately closed.
"([{}])" β True β pairs nest properly: the innermost {} closes first, then [], then ().
"(]" β False β the closer ] does not match the most recent opener (.
"((" β False β two openers are never closed.
Constraints
1 <= s.length <= 10^4
s consists only of the characters ()[]{}
The length bound means a repeated-rescanning solution (O(n^2)) is already shaky; the expected solution is a single O(n) pass.
Think about it first
Hint 1
Think about what makes `"(]"` invalid: when you reach a closing bracket, only one specific opener is allowed to be "waiting" for it. Which one?
Hint 2
The bracket that was opened most recently must be the first one to close. "Most recent thing first" is exactly what a LIFO structure β a stack β gives you in O(1).
Hint 3
Scan left to right. Push every opener. On a closer, pop the stack and check that the popped opener is the matching type (fail if the stack is empty or the types differ). The string is valid iff you never fail and the stack is empty at the end.
TL;DR
One-pass stack of unmatched openers β O(n) time, O(n) space.
Approach 1 β Brute force
A balanced string always contains at least one literal adjacent pair ((), [], or {}). Delete such pairs repeatedly; a valid string shrinks to empty, an invalid one gets stuck.
class Solution:
def isValid(self, s: str) -> bool:
changed = True
while changed:
changed = False
for pair in ("()", "[]", "{}"):
if pair in s:
s = s.replace(pair, "")
changed = True
return s == ""
Complexity: O(n^2) time (each sweep is O(n) and may remove only one pair; up to n/2 sweeps), O(n) space for the rebuilt strings.
Why the constraints kill it: with n = 10^4 a deeply nested string like ((((...)))) forces ~5,000 full-string rescans and rebuilds β roughly 10^7β10^8 character operations, needlessly slow when one pass suffices.
Approach 2 β Stack
The insight: validity is a nesting property. At any point in the scan, the only closer that can legally appear is the one matching the most recently opened, not-yet-closed bracket. A stack stores exactly that: the sequence of still-open brackets, most recent on top. So push openers, and on every closer pop once and compare types.
class Solution:
def isValid(self, s: str) -> bool:
match = {")": "(", "]": "[", "}": "{"}
stack: list[str] = []
for ch in s:
if ch in match: # ch is a closer
if not stack or stack.pop() != match[ch]:
return False
else: # ch is an opener
stack.append(ch)
return not stack # leftovers mean unclosed openers
Walkthrough on "([{}])":
| step | ch | action | stack after |
|---|
| 1 | ( | push | ( |
| 2 | [ | push | ( [ |
| 3 | { | push | ( [ { |
| 4 | } | pop {, matches | ( [ |
| 5 | ] | pop [, matches | ( |
| 6 | ) | pop (, matches | (empty) |
Stack is empty at the end β True. On "(]": push (, then ] pops ( which does not match [ β False immediately.
Complexity: O(n) time β each character is pushed and popped at most once. O(n) space for the stack (worst case all openers).
Common pitfalls
- Popping an empty stack: input like
"]" starts with a closer; guard with if not stack before popping or you get an exception (or a wrong answer).
- Forgetting the final emptiness check:
"((" never triggers a mismatch β you must return not stack, not True, at the end.
- Matching only counts, not types: counting opens minus closes accepts
"([)]"; the stack comparison of types is what rejects interleaved pairs.
- Early exit: if
len(s) is odd you can return False immediately β a micro-optimization, but donβt let it replace the real checks.
Pattern takeaway
When a problem says βthe most recent unfinished thing must be resolved first,β reach for a stack: push work as it opens, pop and validate as it closes, and treat a non-empty stack at the end as unfinished business. This exact push-opener/pop-on-closer skeleton reappears in expression parsing, HTML tag matching, and Decode String.