# 括号生成 (Generate Parentheses) ## 题目描述 数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。 ### 示例 **示例 1:** ``` 输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"] ``` **示例 2:** ``` 输入:n = 1 输出:["()"] ``` ### 约束条件 - `1 <= n <= 8` ## 解题思路 ### 方法一:回溯法(推荐) **核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性: 1. 左括号数量不能超过 n 2. 右括号数量不能超过左括号数量 **算法步骤:** 1. 初始化结果数组 `result` 和当前字符串 `current` 2. 定义回溯函数 `backtrack(open, close)`: - `open`:已使用的左括号数量 - `close`:已使用的右括号数量 3. 终止条件:`len(current) == 2 * n`,将 `current` 加入 `result` 4. 选择条件: - 如果 `open < n`,可以添加左括号 - 如果 `close < open`,可以添加右括号 5. 递归调用后撤销选择(回溯) **为什么这样做?** - 通过限制 `close < open`,保证任何时候右括号数量不超过左括号数量 - 通过限制 `open < n`,保证左括号数量不超过 n - 这样生成的所有组合都是有效的 ### 方法二:DFS 深度优先搜索 **核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。 **算法步骤:** 1. 构建一个递归树,每个节点代表一个状态 2. 从根节点开始,每次可以选择添加左括号或右括号 3. 剪枝:不符合条件的分支直接跳过 4. 到达叶子节点(长度为 2n)时,记录结果 ### 方法三:动态规划 **核心思想:**利用卡特兰数(Catalan Number)的性质。n 对括号的有效组合数等于第 n 个卡特兰数。 **递推公式:** - `dp[n]` 表示 n 对括号的所有有效组合 - `dp[n] = "(" + dp[i] + ")" + dp[n-1-i]`,其中 `i` 从 0 到 n-1 **算法步骤:** 1. 初始化 `dp[0] = [""]` 2. 对于 `i` 从 1 到 n: - 对于 `j` 从 0 到 i-1: - 将 `dp[j]` 的每个组合加上一对括号,再拼接 `dp[i-1-j]` 的每个组合 3. 返回 `dp[n]` ## 代码实现 ### 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 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] } ``` - **时间复杂度:** O(4^n / √n) - 在回溯树中,每个节点最多有 2 个分支 - 树的高度为 2n - 但是由于剪枝,实际复杂度约为卡特兰数 C(n) - 卡特兰数约为 O(4^n / (n^(3/2) * √π)) - **空间复杂度:** O(n) - 递归栈深度最大为 2n - 存储结果的空间不算在内(这是必须的) ### 动态规划 - **时间复杂度:** O(4^n / √n) - 与回溯法类似,需要生成所有有效组合 - **空间复杂度:** O(4^n / √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: 不同的二叉搜索树