Skip to content

22. Generate Parentheses (Medium)

Problem

Given n pairs of parentheses, generate all combinations of well-formed parentheses.

Example

  • n = 3["((()))","(()())","(())()","()(())","()()()"]
  • n = 1["()"]

LeetCode 22 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, generate every 2n-char string, filter valid

Enumerate all 2^(2n) strings over {(, )}, keep the valid ones.

def generate_parenthesis(n: int) -> list[str]:
def is_valid(s): # helper: O(n) per call
balance = 0
for ch in s:
balance += 1 if ch == "(" else -1
if balance < 0:
return False
return balance == 0
result = []
def rec(s): # L1: recursive enumeration
if len(s) == 2 * n: # L2: O(1) base-case check
if is_valid(s): # L3: O(n) validation
result.append(s)
return
rec(s + "(") # L4: recurse with open
rec(s + ")") # L5: recurse with close
rec("")
return result

Where the time goes, line by line

Variables: n = number of pairs, C(n) = n-th Catalan number.

LinePer-call costTimes executedContribution
L1-L5 (full enumeration)O(n)O(n) at leaves4^n nodesO(4nn)O(4^n · n) ← dominates
L3 (is_valid at leaf)O(n)O(n)4^n leavesO(4nn)O(4^n · n)

The recursion tree has 4^n leaves (all 2n-length strings of ( and )); each is validated in O(n)O(n).

Complexity

  • Time: O(22nn)O(2^{2n} \cdot n), driven by L1-L5 (exponentially many strings, each validated in O(n)O(n)).
  • Space: O(n)O(n) recursion depth (plus output).

Approach 2: Prune invalid strings during generation

Track running balance during the recursion. Abort a branch as soon as close > open or open > n.

def generate_parenthesis(n: int) -> list[str]:
result = []
def rec(s, opens, closes): # L1: pruned recursion
if opens > n or closes > opens: # L2: O(1) prune check
return
if len(s) == 2 * n: # L3: O(1) base case
result.append(s)
return
rec(s + "(", opens + 1, closes) # L4: try open
rec(s + ")", opens, closes + 1) # L5: try close
rec("", 0, 0)
return result

Where the time goes, line by line

Variables: n = number of pairs, C(n) = n-th Catalan number.

LinePer-call costTimes executedContribution
L2 (prune check)O(1)O(1)at each nodereduces tree to ~4·C(n) nodes
L4, L5 (recurse)O(n)O(n) at leavesC(n) leavesO(C(n)O(C(n) · n) ← dominates

Pruning reduces the explored space from 4^n to the Catalan number C(n) ≈ 4^n / (n^(3/2) · sqrt(pi)).

Complexity

  • Time: O(4n/sqrt(n)O(4^n / sqrt(n)), the n-th Catalan number times a linear factor. Much smaller than the brute-force 4^n.
  • Space: O(n)O(n) recursion depth.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Backtracking with the exact open/close invariant (optimal)

Same idea, phrased so the invariants are explicit: at each step you can add ( if opens < n, and ) if closes < opens.

def generate_parenthesis(n: int) -> list[str]:
result = [] # L1: O(1)
path = [] # L2: O(1) shared path buffer
def backtrack(opens, closes):
if opens == n and closes == n: # L3: O(1) done check
result.append("".join(path)) # L4: O(n) join at leaf
return
if opens < n: # L5: O(1)
path.append("(") # L6: O(1) amortized
backtrack(opens + 1, closes) # L7: recurse
path.pop() # L8: O(1) undo
if closes < opens: # L9: O(1)
path.append(")") # L10: O(1) amortized
backtrack(opens, closes + 1) # L11: recurse
path.pop() # L12: O(1) undo
backtrack(0, 0)
return result

Where the time goes, line by line

Variables: n = number of pairs, C(n) = n-th Catalan number.

LinePer-call costTimes executedContribution
L3-L4 (base case + join)O(n)O(n)C(n) leavesO(C(n)O(C(n) · n)
L5-L12 (append/recurse/pop)O(1)O(1) amortized~4·C(n) nodesO(C(n)O(C(n))
TotalO(C(n)O(C(n) · n) ← dominates

Using a single path list with append/pop avoids O(n)O(n) string concatenation on each internal node; the join cost O(n)O(n) is paid only at the C(n) leaves.

Complexity

  • Time: O(4n/sqrt(n)O(4^n / sqrt(n)), driven by L4/L7/L11 (Catalan-many recursive calls). Same asymptotic as Approach 2; the count of well-formed sequences is the n-th Catalan number.
  • Space: O(n)O(n) recursion depth + output.

Using a single path list with append/pop avoids string concatenation (O(n)O(n) per append); the final join happens only on full paths.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
Enumerate all + validateO(22nn)O(2^{2n} \cdot n)O(n)O(n)
Prune during generationO(4n/sqrt(n)O(4^n / sqrt(n))O(n)O(n)
Backtracking with invariantsO(4n/sqrt(n)O(4^n / sqrt(n))O(n)O(n)

Approach 3 is the canonical answer and the cleanest expression of the idea. The output is inherently exponential, so you can’t beat the Catalan bound.

Test cases

# Quick smoke tests, paste into a REPL or save as test_generate_parentheses.py and run.
# Uses the canonical implementation (Approach 3: backtracking with invariants).
def generate_parenthesis(n: int) -> list[str]:
result = []
path = []
def backtrack(opens, closes):
if opens == n and closes == n:
result.append("".join(path))
return
if opens < n:
path.append("(")
backtrack(opens + 1, closes)
path.pop()
if closes < opens:
path.append(")")
backtrack(opens, closes + 1)
path.pop()
backtrack(0, 0)
return result
def _run_tests():
assert sorted(generate_parenthesis(1)) == ["()"]
assert sorted(generate_parenthesis(2)) == sorted(["(())", "()()"])
assert sorted(generate_parenthesis(3)) == sorted(["((()))","(()())","(())()","()(())","()()()"])
assert len(generate_parenthesis(4)) == 14 # 4th Catalan number
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Stacks, the recursion stack is the backtracking frontier
  • Backtracking, the explore, undo, and prune pattern for building candidates.
  • Stack Parsing, the last open, first closed model for nested syntax and reversible operations.