Parentheses are a fundamental concept in computer programming and mathematics to control order of operations and group expressions. Generating all possible valid combinations of parentheses is a common technical interview coding challenge. This task tests a developer’s understanding of recursion, stacks, tree traversal, and dynamic programming.
In this comprehensive guide, we will examine different methods to generate valid parentheses combinations in Python.
Table of Contents
Open Table of Contents
Overview
The problem statement is:
Given an integer n, generate all possible valid combinations of n pairs of parentheses.
For example, if n = 3, the valid combinations are:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
Some key points:
- Parentheses must be balanced - every opening bracket must have a matching closing bracket.
- The number of left and right parentheses must be equal to n.
- Order matters - ”(()))” is invalid while ”((()))” is valid.
- Cannot have more than n pairs of parentheses.
This type of combinatorial generation problem lends itself well to recursion and dynamic programming solutions. We will examine 5 different methods:
- Recursive Backtracking
- Iterative Using Stack
- Recursive with Memoization
- Iterative Dynamic Programming
- BFS Tree Traversal
For each approach, we will break down the key steps with example code and explanations. Pros and cons of each method will also be analyzed.
Method 1: Recursive Backtracking
Recursive backtracking is a straightforward technique for this problem. We start with an empty string, then recursively add either a left or right parenthesis at each step. Backtracking allows exploring all possible combinations.
Python Implementation
def generate_parens(n):
combinations = []
generate(combinations, "", 0, 0, n)
return combinations
def generate(combinations, current, openN, closeN, max):
if len(current) == max*2:
combinations.append(current)
return
if openN < max:
generate(combinations, current+"(", openN+1, closeN, max)
if closeN < openN:
generate(combinations, current+")", openN, closeN+1, max)
How it Works
- Base cases are when we reach the maximum number of parentheses, we add current combination to output list.
- Recursively call generate, increasing open or close based on constraints.
- Key is tracking number of open and close parens added so far.
- Try adding left parenthesis if number of open parens < n.
- Try right parenthesis if close parens < open parens (to keep valid).
Analysis
- Simple recursive backtracking is easy to implement.
- However, repeatedly exploring the same subpaths leads to exponential time complexity O(2^n). Not efficient for larger n.
- Recursive calls with string copying at each step also requires O(n) space complexity.
Method 2: Iterative Using Stack
We can optimize the backtracking approach by using a stack to iteratively build the combinations.
Python Implementation
def generate_parens(n):
combinations = []
stack = [('(', 1, 0)]
while stack:
current, openN, closeN = stack.pop()
if len(current) == n*2:
combinations.append(current)
else:
if openN < n:
stack.append((current+'(', openN+1, closeN))
if closeN < openN:
stack.append((current+')', openN, closeN+1))
return combinations
How it Works
- Use stack to track current combination string along with open/close counts.
- Push next combinations by appending ( if open < n, ) if close < open.
- Pop finished combinations and continue building.
- Avoid repeated recursive calls.
Analysis
- Iterative stack-based approach improves time complexity to O(n x 2^n) by removing function call overhead.
- Space complexity reduced to O(n) by iteratively updating a single combination string rather than recursing on copies.
Method 3: Recursive with Memoization
We can optimize the recursive backtracking solution using memoization - storing results of solved subproblems to avoid recomputing them.
Python Implementation
from functools import lru_cache
@lru_cache(maxsize=None)
def generate_parens(n):
if n == 0:
return ['']
combinations = []
for c in generate_parens(n-1):
combinations.append('('+c+')')
combinations.append('()'+c)
combinations.append(c+'()')
return combinations
How it Works
- Decorating the recursive function with lru_cache memoizes results.
- For a given n, recursively call on n-1 and add ’()’ around previous results.
- Avoids re-exploring same subpaths by caching prior results.
Analysis
- Memoization reduces exponential time to polynomial O(n x 4^n) by caching subproblems.
- But still O(n) space complexity to store recursive call stack.
Method 4: Iterative Dynamic Programming
We can further optimize using iterative bottom-up dynamic programming.
Python Implementation
def generate_parens(n):
combinations = [['']]
for i in range(1,n+1):
temp = []
for c in combinations[i-1]:
temp.append('('+c+')')
temp.append('()'+c)
temp.append(c+'()')
combinations.append(temp)
return combinations[-1]
How it Works
- Build up combinations for n by extending combinations for n-1.
- Avoid recursion using bottom-up iterative dynamic programming.
Analysis
- Runs in O(n x 4^n) time by iteratively combining prior results.
- Constant O(n) space needed for current/prior combination arrays.
- Most optimal method overall.
Method 5: BFS Tree Traversal
We can model this problem as traversing a tree breadth-first and collect valid leaf nodes.
Python Implementation
from collections import deque
def generate_parens(n):
queue = deque([('(', 1, 0)])
combinations = []
while queue:
current, openN, closeN = queue.popleft()
if len(current) == n*2:
combinations.append(current)
else:
if openN < n:
queue.append((current+'(', openN+1, closeN))
if closeN < openN:
queue.append((current+')', openN, closeN+1))
return combinations
How it Works
- Use BFS queue to traverse tree level by level.
- Append ( to current if open < n, ) if close < open.
- Leaf nodes give valid combinations.
Analysis
- Generates combinations in lexicographic order.
- Time complexity O(n x C(2n, n)) as we build full tree.
- Space O(n) for queue.
- Slower and more memory intensive than dynamic programming.
Summary and Recommendations
- Backtracking recursion is simple to implement but inefficient without optimization.
- Stack-based iterative backtracking improves time complexity.
- Memoization reduces repeated work by caching prior results.
- Iterative bottom-up dynamic programming is the most optimal approach.
- BFS traversal generates combinations in lexicographic order.
For interview coding questions, I recommend either the iterative stack-based or dynamic programming solutions, which offer the best time/space complexity. Make sure to explain the algorithm and analysis to the interviewer.
Some follow-up opportunities:
- Return combinations in lexicographic sorted order.
- Handle invalid parentheses sequences.
- Generate expressions with minimum removals to make valid.
In summary, generating valid parentheses combinations evaluates fundamental coding concepts like recursion, trees, dynamic programming, and backtracking search. Mastering this question demonstrates strong technical and analytical skills for programming interviews.