# 括号生成 (Generate Parentheses) ## 题目描述 数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。 ### 示例 **示例 1:** ``` 输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"] ``` **示例 2:** ``` 输入:n = 1 输出:["()"] ``` ### 约束条件 - `1 <= n <= 8` ## 思路推导 ### 暴力解法分析 **第一步:最直观的思路 - 生成所有可能的组合** ```python def generateParenthesis_brute(n): # 生成所有长度为 2n 的括号组合 all_combinations = [] generate_all("", 2*n, all_combinations) # 过滤出有效的组合 valid_combinations = [] for combo in all_combinations: if is_valid(combo): valid_combinations.append(combo) return valid_combinations def generate_all(current, max_len, result): if len(current) == max_len: result.append(current) return generate_all(current + "(", max_len, result) generate_all(current + ")", max_len, result) def is_valid(s): balance = 0 for char in s: if char == "(": balance += 1 else: balance -= 1 if balance < 0: return False return balance == 0 ``` **时间复杂度分析:** - 生成所有组合:每个位置有 2 种选择,共 2^(2n) 个组合 - 判断每个组合是否有效:O(n) - **总时间复杂度:O(n × 2^(2n))** **问题:** - 生成了大量无效组合 - 对于 n = 8,2^(16) = 65536 个组合,但有效的只有卡特兰数 C(8) = 1430 个 - 效率极低,大部分计算都浪费了 ### 优化思考 - 如何避免生成无效组合? **核心观察:** 1. **暴力法的瓶颈**:生成了大量无效组合,最后才过滤 2. **优化方向**:能否在生成过程中就避免无效组合? **关键问题:什么是无效组合?** 无效组合的特征: 1. 右括号数量超过左括号数量:")()(" 2. 最终左右括号数量不相等:"((()" **思路:在生成过程中实时检查** 与其生成所有组合再过滤,不如: 1. 在添加每个括号时就检查是否合法 2. 如果不合法,直接跳过这个分支(剪枝) 3. 只生成有可能合法的组合 ```python def generateParenthesis_optimized(n): result = [] def backtrack(current, open_count, close_count): # 终止条件 if len(current) == 2 * n: result.append(current) return # 关键剪枝条件 # 1. 左括号数量不能超过 n if open_count < n: backtrack(current + "(", open_count + 1, close_count) # 2. 右括号数量不能超过左括号数量 if close_count < open_count: backtrack(current + ")", open_count, close_count + 1) backtrack("", 0, 0) return result ``` ### 为什么这样思考? **1. 剪枝思想(Pruning)** ``` 暴力法:生成所有 2^(2n) 个组合,然后过滤 ↓ 优化:在生成过程中就剪掉不可能的分支 ↓ 效果:只生成卡特兰数 C(n) 个有效组合 ``` **2. 有效括号的本质** ``` 有效括号的两个充要条件: 1. 任何时候,左括号数量 >= 右括号数量 2. 最终,左括号数量 = 右括号数量 = n 这意味着: - 可以添加左括号的条件:open < n - 可以添加右括号的条件:close < open ``` **3. 递归树视角** ``` n = 2 时的递归树: "" (open=0, close=0) ├── "(" (open=1, close=0) │ ├── "((" (open=2, close=0) │ │ └── "(()" (open=2, close=1) │ │ └── "(())" (open=2, close=2) ✓ │ └── "()" (open=1, close=1) │ └── "()(" (open=2, close=1) │ └── "()()" (open=2, close=2) ✓ └── ")" (open=0, close=0) ✗ 剪枝:close 不能 > open 只生成了 2 个有效组合,而不是 2^4 = 16 个! ``` ## 解题思路 ### 方法一:回溯法(推荐) **核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性。 ### 详细算法流程 **步骤1:理解有效括号的生成规则** ```python # 规则1:左括号数量不能超过 n if open < n: 可以添加 '(' # 规则2:右括号数量不能超过左括号数量 if close < open: 可以添加 ')' ``` **Q: 为什么右括号数量不能超过左括号数量?** A: 因为在任何前缀中,如果右括号多于左括号,就不可能通过后续添加括号使其变成有效括号。 举例: ``` ")" → 不可能变有效,因为第一个字符就是右括号 "())(" → 中间的 "()" 后面是 ")",右括号已经多余了 ``` **步骤2:设计回溯函数** ```python def backtrack(current, open, close): # 终止条件:生成了足够的括号 if len(current) == 2 * n: result.append(current) return # 选择1:添加左括号(如果可以) if open < n: backtrack(current + "(", open + 1, close) # 选择2:添加右括号(如果可以) if close < open: backtrack(current + ")", open, close + 1) ``` **Q: 为什么不需要显式地撤销选择(回溯)?** A: 因为我们使用的是字符串拼接 `current + "("`,这会创建新的字符串,不会修改原来的 `current`。每次递归调用都是独立的,不需要手动撤销。 如果要使用列表优化性能: ```python def backtrack(current, open, close): if len(current) == 2 * n: result.append("".join(current)) return if open < n: current.append("(") # 做选择 backtrack(current, open + 1, close) current.pop() # 撤销选择 if close < open: current.append(")") # 做选择 backtrack(current, open, close + 1) current.pop() # 撤销选择 ``` **步骤3:初始化并启动回溯** ```python result = [] backtrack("", 0, 0) return result ``` **Q: 为什么初始状态是 open=0, close=0?** A: 因为我们从空字符串开始,还没有添加任何括号。 ### 关键细节说明 **细节1:为什么终止条件是 `len(current) == 2 * n`?** ```python # 错误理解:应该是 len(current) == n # 错误原因:n 是括号的对数,每对有 2 个括号 # 正确理解:总长度是 2n # 例如 n=3,最终字符串长度是 6:"((()))" ``` **细节2:为什么两个 if 是独立的,而不是 if-else?** ```python # 错误写法:if-else if open < n: backtrack(current + "(", open + 1, close) else: backtrack(current + ")", open, close + 1) # 问题:这样会导致每步只能添加一种括号 # 正确写法:两个独立的 if if open < n: backtrack(current + "(", open + 1, close) if close < open: backtrack(current + ")", open, close + 1) ``` **为什么?** - 因为在满足两个条件的情况下,我们可以选择添加左括号或右括号 - 这是两种不同的选择,需要分别探索 **细节3:为什么判断条件是 `close < open` 而不是 `close <= open`?** ```python # 错误写法 if close <= open: # ❌ backtrack(current + ")", open, close + 1) # 问题:当 close == open 时,不能添加右括号 # 例如:current="()", open=1, close=1 # 如果再添加 ")",变成 "()()",这是错误的 # 正确写法 if close < open: # ✓ backtrack(current + ")", open, close + 1) ``` ### 边界条件分析 **边界1:n = 1** ``` 输入:n = 1 输出:["()"] 过程: "" → "(" → "()" 终止:len("()") = 2 = 2*1 ``` **边界2:n = 8(最大值)** ``` 输入:n = 8 输出:1430 个组合(卡特兰数 C(8)) 注意:虽然约束是 n <= 8,但算法可以处理更大的 n ``` **边界3:某个时刻右括号用完了** ``` 状态:current="(()(", open=3, close=1 分析: - 不能添加 ")":因为 close(1) < open(3),可以添加 - 添加后:current="(()()", open=3, close=2 - 继续添加 ")":current="(()())", open=3, close=3 ✓ ``` ### 复杂度分析(详细版) **时间复杂度:** ``` - 理论最坏情况:每个节点有 2 个分支,深度为 2n → O(2^(2n)) - 实际情况:由于剪枝,只有卡特兰数 C(n) 个有效节点 - 卡特兰数公式:C(n) = (2n)! / ((n+1)! × n!) - 渐近复杂度:C(n) ≈ 4^n / (n^(3/2) × √π) - **时间复杂度:O(4^n / √n)** 为什么是 4^n 而不是 2^(2n)? - 数学上 2^(2n) = 4^n - 但由于剪枝,实际是 O(4^n / √n),比 O(4^n) 小得多 ``` **空间复杂度:** ``` - 递归栈深度:最多 2n 层(每添加一个括号递归一次)- O(n) - 存储结果:O(C(n))(卡特兰数)- 通常不计入空间复杂度 - **空间复杂度:O(n)**(不计结果存储) ``` ### 方法二:动态规划 **核心思想:**利用卡特兰数的递推关系。 **递推公式:** ``` dp[n] = "(" + dp[i] + ")" + dp[n-1-i] 其中 i 从 0 到 n-1 解释: - dp[n] 表示 n 对括号的所有有效组合 - 每个组合可以看作:一对括号包裹着 i 对括号,后面跟着 n-1-i 对括号 ``` **Q: 为什么这样递推?** A: 任何有效的 n 对括号组合,都可以分解为: 1. 第一个左括号 2. 一个与之匹配的右括号 3. 中间有 i 对括号 4. 后面有 n-1-i 对括号 举例: ``` "(()())" 可以分解为: ( + ()() + ) ↑ ↑ ↑ | | 匹配的右括号 | 中间的 i=2 对括号 第一个左括号 所以:"(())()" = "(" + "()" + ")" + "()" i=1, n-1-i=1 ``` ## 代码实现 ### Go 实现(回溯法) ```go package main import "fmt" func generateParenthesis(n int) []string { result := []string{} current := []byte{} var backtrack func(open, close int) backtrack = func(open, close int) { // 终止条件:生成了 2n 个括号 if len(current) == 2*n { result = append(result, string(current)) return } // 添加左括号:左括号数量小于 n if open < n { current = append(current, '(') backtrack(open+1, close) current = current[:len(current)-1] // 回溯 } // 添加右括号:右括号数量小于左括号数量 if close < open { current = append(current, ')') backtrack(open, close+1) current = current[:len(current)-1] // 回溯 } } backtrack(0, 0) return result } // 测试用例 func main() { // 测试用例1 n1 := 3 fmt.Printf("输入: n = %d\n", n1) fmt.Printf("输出: %v\n", generateParenthesis(n1)) // 测试用例2 n2 := 1 fmt.Printf("\n输入: n = %d\n", n2) fmt.Printf("输出: %v\n", generateParenthesis(n2)) // 测试用例3 n3 := 4 fmt.Printf("\n输入: n = %d\n", n3) result3 := generateParenthesis(n3) fmt.Printf("输出长度: %d\n", len(result3)) fmt.Printf("输出: %v\n", result3) // 验证卡特兰数 for i := 1; i <= 8; i++ { fmt.Printf("n = %d, 组合数 = %d\n", i, len(generateParenthesis(i))) } } ``` ### Go 实现(动态规划) ```go func generateParenthesisDP(n int) []string { if n == 0 { return []string{""} } dp := make([][]string, n+1) dp[0] = []string{""} for i := 1; i <= n; i++ { dp[i] = []string{} for j := 0; j < i; j++ { for _, left := range dp[j] { for _, right := range dp[i-1-j] { dp[i] = append(dp[i], "("+left+")"+right) } } } } return dp[n] } ``` ## 执行过程演示 以 `n = 3` 为例: ``` 初始状态: current="", open=0, close=0 第1层递归: current="" → 可以添加 "(" (open=0 < 3) current="" → 不能添加 ")" (close=0 不 < open=0) 路径:backtrack("(", 1, 0) 第2层递归: current="(" → 可以添加 "(" (open=1 < 3) current="(" → 可以添加 ")" (close=0 < open=1) 路径1:backtrack("((", 2, 0) 路径2:backtrack("()", 1, 1) 第3层递归(路径1): current="((" → 可以添加 "(" (open=2 < 3) current="((" → 不能添加 ")" (close=0 < open=2) ✓ 路径1.1:backtrack("(((", 3, 0) 路径1.2:backtrack("(()", 2, 1) 第4层递归(路径1.1): current="(((" → 不能添加 "(" (open=3 不 < 3) current="(((" → 不能添加 ")" (close=0 < open=3) ✓ 路径1.1.1:backtrack("((()", 3, 1) 第5层递归(路径1.1.1): current="((()" → 不能添加 "(" (open=3 不 < 3) current="((()" → 不能添加 ")" (close=1 < open=3) ✓ 路径1.1.1.1:backtrack("((())", 3, 2) 第6层递归(路径1.1.1.1): current="((())" → 不能添加 "(" (open=3 不 < 3) current="((())" → 不能添加 ")" (close=2 < open=3) ✓ 路径1.1.1.1.1:backtrack("((()))", 3, 3) 第7层递归(路径1.1.1.1.1): current="((()))" → len=6=2*3,终止! 添加到结果:["((()))"] ...(继续其他路径) 最终结果: ["((()))","(()())","(())()","()(())","()()()"] ``` ## 常见错误 ### 错误1:忘记剪枝条件 ❌ **错误写法:** ```go func generateParenthesisWrong(n int) []string { result := []string{} var backtrack func(current string, length int) backtrack = func(current string, length int) { if length == 2*n { result = append(result, current) return } // 没有剪枝条件! backtrack(current+"(", length+1) backtrack(current+")", length+1) } backtrack("", 0) return result } ``` ✅ **正确写法:** ```go func generateParenthesis(n int) []string { result := []string{} current := []byte{} var backtrack func(open, close int) backtrack = func(open, close int) { if len(current) == 2*n { result = append(result, string(current)) return } if open < n { // 剪枝条件1 current = append(current, '(') backtrack(open+1, close) current = current[:len(current)-1] } if close < open { // 剪枝条件2 current = append(current, ')') backtrack(open, close+1) current = current[:len(current)-1] } } backtrack(0, 0) return result } ``` **原因:**没有剪枝会生成大量无效组合,时间复杂度爆炸。 ### 错误2:使用 if-else 而不是独立的 if ❌ **错误写法:** ```go if open < n { backtrack(open+1, close) } else if close < open { backtrack(open, close+1) } ``` ✅ **正确写法:** ```go if open < n { backtrack(open+1, close) } if close < open { backtrack(open, close+1) } ``` **原因:**两个条件可能同时满足,需要都尝试。 ### 错误3:终止条件错误 ❌ **错误写法:** ```go if open == n && close == n { // 错误 result = append(result, string(current)) return } ``` ✅ **正确写法:** ```go if len(current) == 2*n { // 正确 result = append(result, string(current)) return } ``` **原因:**虽然 `open==n && close==n` 等价于 `len(current)==2*n`,但前者更冗余。 ## 进阶问题 ### Q1: 如何判断一个括号字符串是否有效? **A:** 使用栈或者计数器。 ```go // 方法1: 使用栈 func isValid(s string) bool { stack := []byte{} for _, c := range []byte(s) { if c == '(' { stack = append(stack, c) } else if len(stack) > 0 { stack = stack[:len(stack)-1] } else { return false } } return len(stack) == 0 } // 方法2: 使用计数器 func isValidSimple(s string) bool { count := 0 for _, c := range s { if c == '(' { count++ } else if c == ')' { count-- } if count < 0 { return false } } return count == 0 } ``` ### Q2: 如果有三种括号 ()、[]、{},应该如何生成? **A:** 需要更复杂的逻辑来保证括号匹配。 ```go func generateMultipleParentheses(n int) []string { types := []byte{'(', ')', '[', ']', '{', '}'} result := []string{} current := []byte{} stack := []byte{} var backtrack func(int) backtrack = func(length int) { if len(current) == 2*n { result = append(result, string(current)) return } for i := 0; i < len(types); i += 2 { // 添加左括号 if length < n { current = append(current, types[i]) stack = append(stack, types[i]) backtrack(length + 1) current = current[:len(current)-1] stack = stack[:len(stack)-1] } } for i := 1; i < len(types); i += 2 { // 添加右括号:必须与栈顶匹配 if len(stack) > 0 && stack[len(stack)-1] == types[i-1] { current = append(current, types[i]) stack = stack[:len(stack)-1] backtrack(length) current = current[:len(current)-1] stack = append(stack, types[i-1]) } } } backtrack(0) return result } ``` ### Q3: 如何优化内存使用,特别是对于大的 n? **A:** 可以使用生成器模式,逐个生成结果而不是全部存储。 ```go func generateParenthesisGenerator(n int, callback func(string)) { current := make([]byte, 0, 2*n) var backtrack func(open, close int) backtrack = func(open, close int) { if len(current) == 2*n { callback(string(current)) return } if open < n { current = append(current, '(') backtrack(open+1, close) current = current[:len(current)-1] } if close < open { current = append(current, ')') backtrack(open, close+1) current = current[:len(current)-1] } } backtrack(0, 0) } ``` ## P7 加分项 ### 1. 深度理解:卡特兰数(Catalan Number) **定义:**卡特兰数是组合数学中经常出现的数列,在许多计数问题中出现。 **公式:** - C(n) = (2n)! / ((n+1)! × n!) - C(n) = C(0)×C(n-1) + C(1)×C(n-2) + ... + C(n-1)×C(0) **前几项:**1, 1, 2, 5, 14, 42, 132, 429, 1430, ... **应用场景:** 1. 括号匹配问题(本题) 2. 二叉搜索树的计数 3. 出栈序列的计数 4. 路径计数(不穿过对角线) **计算卡特兰数:** ```go func catalanNumber(n int) int { if n <= 1 { return 1 } // 动态规划计算 dp := make([]int, n+1) dp[0], dp[1] = 1, 1 for i := 2; i <= n; i++ { for j := 0; j < i; j++ { dp[i] += dp[j] * dp[i-1-j] } } return dp[n] } ``` ### 2. 实战扩展:通用回溯框架 **回溯法通用模板:** ```go func backtrack(路径, 选择列表) { if 满足结束条件 { result = append(result, 路径) return } for 选择 in 选择列表 { // 做选择 路径.add(选择) // 递归 backtrack(路径, 选择列表) // 撤销选择(回溯) 路径.remove(选择) } } ``` **应用示例:排列问题** ```go func permute(nums []int) [][]int { result := [][]int{} current := []int{} used := make([]bool, len(nums)) var backtrack func() backtrack = func() { if len(current) == len(nums) { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) return } for i := 0; i < len(nums); i++ { if used[i] { continue } // 做选择 current = append(current, nums[i]) used[i] = true // 递归 backtrack() // 撤销选择 current = current[:len(current)-1] used[i] = false } } backtrack() return result } ``` ### 3. 变形题目 #### 变形1:最长有效括号 **LeetCode 32:** 给定一个只包含 '(' 和 ')' 的字符串,找出最长有效(正确闭合)括号子串的长度。 ```go func longestValidParentheses(s string) int { maxLen := 0 stack := []int{-1} // 初始化为 -1,便于计算长度 for i, c := range s { if c == '(' { stack = append(stack, i) } else { stack = stack[:len(stack)-1] if len(stack) == 0 { stack = append(stack, i) } else { length := i - stack[len(stack)-1] if length > maxLen { maxLen = length } } } } return maxLen } ``` #### 变形2:不同的二叉搜索树 **LeetCode 96:** 给定 n,求恰好由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种? ```go func numTrees(n int) int { dp := make([]int, n+1) dp[0], dp[1] = 1, 1 for i := 2; i <= n; i++ { for j := 1; j <= i; j++ { dp[i] += dp[j-1] * dp[i-j] } } return dp[n] } ``` #### 变形3:括号分数 **LeetCode 856:** 给定一个平衡括号字符串 S,按下述规则计算该字符串的分数: - `()` 得 1 分 - `AB` 得 `A + B` 分,其中 A 和 B 是平衡括号字符串 - `(A)` 得 `2 × A` 分,其中 A 是平衡括号字符串 ```go func scoreOfParentheses(s string) int { stack := []int{0} // 栈底保存当前层的分数 for _, c := range s { if c == '(' { stack = append(stack, 0) // 新的一层,初始分数为 0 } else { // 弹出当前层的分数 top := stack[len(stack)-1] stack = stack[:len(stack)-1] // 计算分数 if top == 0 { stack[len(stack)-1] += 1 } else { stack[len(stack)-1] += 2 * top } } } return stack[0] } ``` ### 4. 优化技巧 #### 优化1:剪枝优化 在回溯过程中,尽早发现不可能的解并剪枝。 ```go func generateParenthesisOptimized(n int) []string { result := []string{} current := []byte{} var backtrack func(open, close int) backtrack = func(open, close int) { // 剪枝:如果剩余的右括号太多,无法完成 if close > open { return } if len(current) == 2*n { result = append(result, string(current)) return } if open < n { current = append(current, '(') backtrack(open+1, close) current = current[:len(current)-1] } if close < open { current = append(current, ')') backtrack(open, close+1) current = current[:len(current)-1] } } backtrack(0, 0) return result } ``` #### 优化2:迭代优化 使用迭代代替递归,避免栈溢出。 ```go func generateParenthesisIterative(n int) []string { type state struct { current string open int close int } result := []string{} stack := []state{{"", 0, 0}} for len(stack) > 0 { // 弹出栈顶 s := stack[len(stack)-1] stack = stack[:len(stack)-1] if len(s.current) == 2*n { result = append(result, s.current) continue } if s.open < n { stack = append(stack, state{s.current + "(", s.open + 1, s.close}) } if s.close < s.open { stack = append(stack, state{s.current + ")", s.open, s.close + 1}) } } return result } ``` ### 5. 实际应用场景 - **编译器:** 语法分析和表达式求值 - **代码格式化:** 自动添加括号 - **数学表达式:** 验证表达式有效性 - **数据验证:** 检查嵌套结构(如 HTML 标签) ### 6. 面试技巧 **面试官可能会问:** 1. "为什么要用回溯法而不是暴力枚举?" 2. "卡特兰数和这个问题有什么关系?" 3. "如何证明你的算法生成的所有组合都是有效的?" **回答要点:** 1. 回溯法通过剪枝避免了无效组合的生成,效率更高 2. n 对括号的有效组合数等于第 n 个卡特兰数 3. 通过维护 `open` 和 `close` 计数器,保证了右括号永远不超过左括号 ### 7. 相关题目推荐 - LeetCode 22: 括号生成(本题) - LeetCode 17: 电话号码的字母组合 - LeetCode 32: 最长有效括号 - LeetCode 39: 组合总和 - LeetCode 46: 全排列 - LeetCode 78: 子集 - LeetCode 96: 不同的二叉搜索树