Master the balanced parentheses problem — validating matching brackets using a stack, all variants (multiple bracket types, minimum additions to balance, longest valid substring), why stacks are the perfect data structure for bracket matching, and interview patterns.
Problem Definition
A string of brackets is balanced if:
- Every opening bracket has a matching closing bracket
- Brackets close in the correct order (LIFO — last opened, first closed)
Valid: () []{} {[()]} ((()))
Invalid: (] ([)] (() {[}
Implementation — Validate Brackets
def is_valid(s):
"""
Check if string of brackets is valid.
Time: O(n), Space: O(n)
"""
stack = []
matching = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char) # Push opening brackets
elif char in ')]}':
if not stack or stack[-1] != matching[char]:
return False # No match
stack.pop() # Matching bracket found
return len(stack) == 0 # Valid only if all openers were matched
print(is_valid("()[]{}")) # True
print(is_valid("([{}])")) # True
print(is_valid("(]")) # False
print(is_valid("([)]")) # False — wrong order
print(is_valid("")) # True — empty is valid
Variant 1 — Minimum Additions to Make Valid
def min_add_to_make_valid(s):
"""
Return minimum number of bracket additions to make s valid.
Count unmatched opens and closes.
"""
open_count = 0 # Unmatched '('
close_count = 0 # Unmatched ')'
for char in s:
if char == '(':
open_count += 1
else: # char == ')'
if open_count > 0:
open_count -= 1 # Matched with an open
else:
close_count += 1 # Unmatched close
return open_count + close_count # Need one addition per unmatched
print(min_add_to_make_valid("())")) # 1
print(min_add_to_make_valid("(((")) # 3
print(min_add_to_make_valid("()")) # 0
Variant 2 — Longest Valid Parentheses Substring
def longest_valid_parentheses(s):
"""
Find length of longest valid parentheses substring.
Stack-based: track indices.
Time: O(n), Space: O(n)
"""
stack = [-1] # Sentinel: index before start of current valid sequence
max_len = 0
for i, char in enumerate(s):
if char == '(':
stack.append(i) # Push opening bracket index
else: # ')'
stack.pop() # Pop matching opener (or sentinel)
if not stack:
stack.append(i) # New base (unmatched ')')
else:
max_len = max(max_len, i - stack[-1])
return max_len
print(longest_valid_parentheses(")()())")) # 4 — "()()"
print(longest_valid_parentheses("(()")) # 2 — "()"
print(longest_valid_parentheses("")) # 0
Variant 3 — Score of Parentheses
def score_of_parentheses(s):
"""
Calculate score: '()' = 1, outer brackets double inner score.
Stack tracks running totals at each nesting level.
"""
stack = [0] # Start with 0 score at level 0
for char in s:
if char == '(':
stack.append(0) # New level, start at 0
else:
v = stack.pop()
stack[-1] += max(2 * v, 1) # '()' = 1, outer doubles inner
return stack[0]
print(score_of_parentheses("()")) # 1
print(score_of_parentheses("(())")) # 2
print(score_of_parentheses("()()")) # 2
print(score_of_parentheses("(()(()))")) # 6
Variant 4 — Generate All Valid Combinations
A related problem: generate all valid bracket combinations of length 2n.
def generate_parentheses(n):
"""
Generate all valid parentheses combinations of n pairs.
Backtracking: add '(' if open count < n, add ')' if close < open.
Time: O(4^n / sqrt(n)) — Catalan number, Space: O(n) recursion depth
"""
result = []
def backtrack(current, open_count, close_count):
if len(current) == 2 * n:
result.append(current)
return
if open_count < n:
backtrack(current + '(', open_count + 1, close_count)
if close_count < open_count:
backtrack(current + ')', open_count, close_count + 1)
backtrack('', 0, 0)
return result
print(generate_parentheses(3))
# ['((()))', '(()())', '(())()', '()(())', '()()()']
The number of valid combinations for n pairs is the n-th Catalan number: C(n) = (2n choose n) / (n+1). For n=3, C(3) = 5, which matches the output above.
Variant 5 — Remove Invalid Parentheses
Given a string with letters and brackets, remove the minimum number of brackets to make it valid. Return all unique results.
def remove_invalid_parentheses(s):
"""
BFS approach: try removing each bracket at each level.
Stop at first level where valid strings are found.
Time: O(2^n), Space: O(2^n) in worst case
"""
from collections import deque
def is_valid(t):
count = 0
for c in t:
if c == '(': count += 1
elif c == ')':
count -= 1
if count < 0: return False
return count == 0
visited = {s}
queue = deque([s])
result = []
found = False
while queue:
level_size = len(queue)
for _ in range(level_size):
cur = queue.popleft()
if is_valid(cur):
result.append(cur)
found = True
if not found:
for i in range(len(cur)):
if cur[i] in '()':
nxt = cur[:i] + cur[i+1:]
if nxt not in visited:
visited.add(nxt)
queue.append(nxt)
if found:
break
return result if result else ['']
print(remove_invalid_parentheses("()())()")) # ['()()()', '(())()']
Interview Patterns and Tips
Sometimes you need to store more than the bracket itself. Store the index for problems asking about the position of invalid brackets:
def find_invalid_positions(s):
"""Return positions of invalid brackets."""
stack = [] # Store indices of unmatched '('
invalid = set()
for i, char in enumerate(s):
if char == '(':
stack.append(i)
elif char == ')':
if stack:
stack.pop() # Matched
else:
invalid.add(i) # Unmatched ')'
# Remaining in stack are unmatched '('
invalid.update(stack)
return sorted(invalid)
print(find_invalid_positions("(()))(")) # [4, 5]
Pattern 2: Counter Instead of Stack (Only One Bracket Type)
When the problem only involves ( and ), you can replace the stack with a simple integer counter. This drops space complexity from O(n) to O(1):
def is_valid_simple(s):
"""O(1) space for single bracket type problems."""
balance = 0
for c in s:
if c == '(':
balance += 1
elif c == ')':
balance -= 1
if balance < 0:
return False # More ')' than '(' so far
return balance == 0
Pattern 3: Two-Pass for Complex Variants
For minimum removal problems, a forward pass handles unmatched ), and a backward pass handles unmatched (:
def min_remove_to_make_valid(s):
"""Remove minimum chars to make parentheses valid."""
s = list(s)
# Forward pass: mark unmatched ')'
open_count = 0
for i, c in enumerate(s):
if c == '(':
open_count += 1
elif c == ')':
if open_count > 0:
open_count -= 1
else:
s[i] = '' # Mark for removal
# Backward pass: mark excess '('
close_count = 0
for i in range(len(s) - 1, -1, -1):
if s[i] == '(':
if close_count > 0:
close_count -= 1
else:
s[i] = '' # Mark for removal
elif s[i] == ')':
close_count += 1
return ''.join(s)
print(min_remove_to_make_valid("lee(t(c)o)de)")) # "lee(t(c)o)de"
print(min_remove_to_make_valid("a)b(c)d")) # "ab(c)d"
Common Edge Cases to Test
Always test your bracket validator against these inputs:
| Input | Expected | Why |
|---|
"" | Valid | Empty string is valid |
"(" | Invalid | Unclosed bracket |
")" | Invalid | Unmatched close |
"()" | Valid | Single pair |
"([{}])" | Valid | Nested, different types |
"([)]" | Invalid | Wrong nesting order |
"(((" | Invalid | Three unclosed |
"))((" | Invalid | Wrong order overall |
"a(b)c" | Valid | Letters mixed in |
Summary
Balanced parentheses problems are solved with a stack because:
- Opening brackets are pushed (remembered for later matching)
- Closing brackets trigger a pop and comparison with the top
- Stack empty at end = all brackets matched
Variants to know:
- Basic validation: O(n) time, O(n) space
- Minimum additions: O(n) time, O(1) space (count unmatched)
- Longest valid substring: O(n) time, O(n) space (track indices)
- Score: O(n) time, O(n) space (track running sums per level)
- Generate all: O(4^n/√n) time, backtracking
- Remove invalid: O(2^n) time, BFS level by level
The counter-based O(1) space solution works when only one bracket type is involved. For multiple bracket types, a stack is essential.
*Next Lesson: Infix Prefix Postfix*