Files
interview/16-LeetCode Hot 100/括号生成.md
yasinshaw a5736a4db7 docs: improve solution explanations for 最长回文子串 and 括号生成
- 添加思路推导部分,从暴力解法分析优化过程
- 增加详细的算法流程和Q&A形式的解释
- 添加执行过程演示和常见错误分析
- 完善边界条件和复杂度分析
- 保持原有的代码实现和进阶问题
2026-03-08 21:32:02 +08:00

1032 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 括号生成 (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 = 82^(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)
```
### 边界条件分析
**边界1n = 1**
```
输入n = 1
输出:["()"]
过程:
"" → "(" → "()"
终止len("()") = 2 = 2*1
```
**边界2n = 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)
路径1backtrack("((", 2, 0)
路径2backtrack("()", 1, 1)
第3层递归路径1
current="((" → 可以添加 "(" (open=2 < 3)
current="((" → 不能添加 ")" (close=0 < open=2) ✓
路径1.1backtrack("(((", 3, 0)
路径1.2backtrack("(()", 2, 1)
第4层递归路径1.1
current="(((" → 不能添加 "(" (open=3 不 < 3)
current="(((" → 不能添加 ")" (close=0 < open=3) ✓
路径1.1.1backtrack("((()", 3, 1)
第5层递归路径1.1.1
current="((()" → 不能添加 "(" (open=3 不 < 3)
current="((()" → 不能添加 ")" (close=1 < open=3) ✓
路径1.1.1.1backtrack("((())", 3, 2)
第6层递归路径1.1.1.1
current="((())" → 不能添加 "(" (open=3 不 < 3)
current="((())" → 不能添加 ")" (close=2 < open=3) ✓
路径1.1.1.1.1backtrack("((()))", 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: 不同的二叉搜索树