Stack for Parentheses Matching
Push opening brackets onto a stack; pop on each closing bracket and verify the pair matches. This one rule validates arbitrarily nested bracket expressions in O(n) and extends naturally to minimum removal, score computation, and longest valid substring.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- The code
- Watch it happen, frame by frame
- Extension 1: Minimum Remove to Make Valid (LC #1249)
- Extension 2: Longest Valid Parentheses (LC #32)
- Extension 3: Score of Parentheses (LC #856)
- Common traps
- Check yourself
- Practice problems
Before we start
The parentheses-matching stack is probably the first real stack problem you will encounter in interviews. It looks trivial at first, but the same core idea extends to five different problem variants — from "is this string valid?" all the way to "what is the score of this expression?" By the end you will be able to:
- Write the basic validity check from memory in under 30 seconds.
- Adapt the core stack to solve minimum-removal, longest-valid-substring, and score variants.
- Explain why a stack is the exact right data structure here (not a counter).
Picture this first (no code yet)
A real-life story
You are proofreading a legal document with nested references: "see clause (A [article 3 {section 2}] and B)." You need to check every opener has a matching closer of the same type, in the right order.
Your strategy: carry a stack of sticky notes. Every time you see an opener (, [, or {, write it on a sticky note and add it to the top of the stack. Every time you see a closer ), ], or }, look at the top sticky note. If it matches, great — discard both. If it doesn't match, or the stack is empty, the document is invalid. After reading the whole document, if the stack is empty, the document is valid.
The stack naturally handles arbitrarily deep nesting because the most recently opened bracket must be the first closed.
The actual problem
Valid Parentheses (LC #20):
Given a string containing only
(,),[,],{,}, determine if it is valid. A string is valid if: every opener has a matching closer of the same type, and they close in the correct order.
"()[]{}"→true."([)]"→false."{[]}"→true."]"→false.
First, the slow way (so you feel the pain)
The naive idea: repeatedly scan the string and remove adjacent matching pairs (), [], {} until no more can be removed. If the string becomes empty, it's valid. For "(((...)))" with 10,000 pairs, each removal shrinks the string by 2 — you scan 10,000 times with up to 20,000 characters each = 2 × 10⁸ character reads. The stack does it in exactly n reads.
The turning point
Pause & think
Why can't we just count? count_open - count_close == 0 at the end?
"([)]" — counts: 2 opens, 2 closes, counts balance. But it is invalid because [ is closed by ) before ( is closed by ].
A counter loses track of which type of bracket is open and in what order. The stack preserves both type and order — which is exactly the information needed.
The one idea to remember
The entire pattern in one sentence
Push openers; on a closer, the stack's top must be the matching opener — if not (or stack empty), invalid; if yes, pop; after scanning, valid iff stack is empty.
The code
def isValid(s: str) -> bool:
stack = []
match = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in '([{':
stack.append(ch)
else:
# closer: stack must be non-empty AND top must match
if not stack or stack[-1] != match[ch]:
return False
stack.pop()
return len(stack) == 0
Line-by-line narration:
stack = []— our stack of unmatched openers.match = {')': '(', ']': '[', '}': '{'}— maps each closer to its required opener.if ch in '([{'— opener: push it.if not stack— hitting a closer when the stack is empty means no opener waiting — invalid.stack[-1] != match[ch]— the most recent opener doesn't match this closer — invalid.stack.pop()— valid pair found; remove the opener.return len(stack) == 0— if unmatched openers remain, invalid.
Watch it happen, frame by frame
s = "{[()]}"
ch='{': opener → push. stack=['{']
ch='[': opener → push. stack=['{','[']
ch='(': opener → push. stack=['{','[','(']
ch=')': closer. top='(' == match[')']='(' ✅ pop. stack=['{','[']
ch=']': closer. top='[' == match[']']='[' ✅ pop. stack=['{']
ch='}': closer. top='{' == match['}']='{' ✅ pop. stack=[]
Loop ends. len(stack)==0 → True ✅
s = "([)]" — the tricky one
ch='(': push. stack=['(']
ch='[': push. stack=['(','[']
ch=')': closer. top='[' ≠ match[')']='(' → return False ✅
Extension 1: Minimum Remove to Make Valid (LC #1249)
Given a string with letters and parentheses, remove the minimum number of brackets to make it valid.
Approach: collect indices of unmatched brackets (not characters, not counts — indices):
def minRemoveToMakeValid(s: str) -> str:
s = list(s)
stack = [] # stores indices of unmatched '('
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
elif ch == ')':
if stack:
stack.pop() # matched — good
else:
s[i] = '' # unmatched ')' — remove immediately
# anything left in stack is unmatched '('
for i in stack:
s[i] = ''
return ''.join(s)
Why store indices? Because we need to mark which ( to remove — specifically the unmatched ones (the ones still in the stack after the full scan).
Extension 2: Longest Valid Parentheses (LC #32)
Find the length of the longest valid parenthesis substring.
Stack approach: store indices. Initialize stack with sentinel [-1] (the "before the string" boundary):
def longestValidParentheses(s: str) -> int:
stack = [-1] # sentinel
result = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
else: # ')'
stack.pop()
if not stack:
stack.append(i) # new boundary: this unmatched ')' index
else:
result = max(result, i - stack[-1])
return result
The sentinel (or the most recent unmatched ) index) marks the "left boundary" of the current valid window. The length of the current valid window is i - stack[-1].
Extension 3: Score of Parentheses (LC #856)
Each () scores 1. Nested (A) scores 2×score(A). Adjacent AB scores score(A)+score(B).
def scoreOfParentheses(s: str) -> int:
stack = [0] # current layer's score
for ch in s:
if ch == '(':
stack.append(0) # open new layer
else:
val = stack.pop()
stack[-1] += max(2 * val, 1) # () → 1; (A) → 2*A
return stack[0]
stack holds the running score of the current nesting level. On (, push 0 (new level). On ), pop the completed level's score: if 0 (was ()), add 1; otherwise add 2× (was nested).
Common traps
Watch out for these
- Checking only stack emptiness, not the bracket type.
"([)]"would pass a counter-only or emptiness-only check. Always verifystack[-1] == match[ch], not justbool(stack). - Forgetting
return len(stack) == 0. If the string is"(((", the loop exits without returning False — but 3 unmatched openers remain. The final check catches this. stack[-1]on an empty stack. Python raises IndexError. Always checkif not stackbefore accessingstack[-1]for closers.- For LC #1249, storing characters instead of indices. You need to mark which specific brackets to remove (not just count them). Indices let you do
s[i] = ''for targeted removal.
Remember this forever
Valid Parentheses
stack = []
match = {')': '(', ']': '[', '}': '{'}
for ch in s:
if ch in '([{':
stack.append(ch)
elif not stack or stack[-1] != match[ch]:
return False
else:
stack.pop()
return not stack
Three extensions:
- Min Remove: stack stores indices of unmatched
(; unmatched)→ clear immediately. - Longest Valid: sentinel
[-1]; length =i - stack[-1]after each successful pop. - Score: stack of layer scores;
)pops →max(2*val, 1)added to parent.
Check yourself
Why does a counter fail for `"([)]"` but a stack succeeds?
A counter only tracks how many brackets are open at each point, not which type or in what order. "([)]" has 2 opens and 2 closes — the counter reaches 0 and says valid. The stack tracks that [ was pushed after (, so when ) arrives, the top is [ (not (), and the mismatch is caught immediately.
In "Longest Valid Parentheses", why do we push the index of an unmatched `)` instead of popping normally?
The index of an unmatched ) acts as a "boundary" — a wall that no valid substring can cross. The next valid window starts after this unmatched ). By pushing its index onto the stack, we ensure i - stack[-1] correctly measures the window from the character after the boundary to the current position. Without this, we'd have nothing to subtract from i and would measure the wrong window length.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Valid Parentheses | Easy | Core push/pop; check type match | LC #20 |
| Minimum Remove to Make Valid | Medium | Store indices of unmatched brackets | LC #1249 |
| Longest Valid Parentheses | Hard | Sentinel [-1]; window = i - stack[-1] | LC #32 |
| Score of Parentheses | Medium | Stack of layer scores; max(2*val, 1) | LC #856 |
| Check if Parentheses String Can Be Valid | Medium | Range of possible open counts | LC #2116 |
Next up: Min/Max Stack — augment a standard stack with an auxiliary stack so that getMin() and getMax() run in O(1) time, regardless of push/pop order.