Files
interview/16-LeetCode Hot 100/括号生成.md
yasinshaw e75e4778b1 feat: add 19 LeetCode Hot 100 medium problems with detailed solutions
批量生成 19 道 LeetCode Hot 100 Medium 难度题目,每道题包含:
- 题目描述和示例
- 多种解题思路(回溯、DP、双指针等)
- Go 和 Java 双语解答
- 完整的测试用例
- 复杂度分析
- 进阶问题
- P7 加分项(深度理解、实战扩展、变形题目)

新增题目:
1. 盛最多水的容器 (Container With Most Water) - LeetCode 11
2. 电话号码的字母组合 (Letter Combinations) - LeetCode 17
3. 删除链表的倒数第N个结点 - LeetCode 19
4. 括号生成 - LeetCode 22
5. 最长回文子串 - LeetCode 5
6. 子集 - LeetCode 78
7. 单词搜索 - LeetCode 79
8. 柱状图中最大的矩形 - LeetCode 84
9. 最大正方形 - LeetCode 221
10. 完全平方数 - LeetCode 279
11. 最长连续序列 - LeetCode 128
12. 除自身以外数组的乘积 - LeetCode 238
13. 最小栈 - LeetCode 155
14. 二叉树的中序遍历 - LeetCode 94
15. 二叉树的最大深度 - LeetCode 104
16. 翻转二叉树 - LeetCode 226
17. 对称二叉树 - LeetCode 101
18. 路径总和 - LeetCode 112
19. 从前序与中序遍历序列构造二叉树 - LeetCode 105

所有代码均包含:
- 清晰的注释说明
- 完整的可运行测试用例
- 时间和空间复杂度分析
- 优化技巧和变形题目

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 12:26:07 +08:00

694 lines
17 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`
## 解题思路
### 方法一:回溯法(推荐)
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性:
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)))
}
}
```
### Java 实现(回溯法)
```java
import java.util.ArrayList;
import java.util.List;
public class GenerateParentheses {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
StringBuilder current = new StringBuilder();
backtrack(result, current, 0, 0, n);
return result;
}
private void backtrack(List<String> result, StringBuilder current,
int open, int close, int max) {
// 终止条件:生成了 2n 个括号
if (current.length() == 2 * max) {
result.add(current.toString());
return;
}
// 添加左括号:左括号数量小于 n
if (open < max) {
current.append('(');
backtrack(result, current, open + 1, close, max);
current.deleteCharAt(current.length() - 1); // 回溯
}
// 添加右括号:右括号数量小于左括号数量
if (close < open) {
current.append(')');
backtrack(result, current, open, close + 1, max);
current.deleteCharAt(current.length() - 1); // 回溯
}
}
// 测试用例
public static void main(String[] args) {
GenerateParentheses solution = new GenerateParentheses();
// 测试用例1
int n1 = 3;
System.out.println("输入: n = " + n1);
System.out.println("输出: " + solution.generateParenthesis(n1));
// 测试用例2
int n2 = 1;
System.out.println("\n输入: n = " + n2);
System.out.println("输出: " + solution.generateParenthesis(n2));
// 测试用例3
int n3 = 4;
System.out.println("\n输入: n = " + n3);
List<String> result3 = solution.generateParenthesis(n3);
System.out.println("输出长度: " + result3.size());
System.out.println("输出: " + result3);
// 验证卡特兰数
System.out.println("\n卡特兰数验证:");
for (int i = 1; i <= 8; i++) {
System.out.println("n = " + i + ", 组合数 = " +
solution.generateParenthesis(i).size());
}
}
}
```
### 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]
}
```
### Java 实现(动态规划)
```java
public List<String> generateParenthesisDP(int n) {
List<List<String>> dp = new ArrayList<>();
List<String> dp0 = new ArrayList<>();
dp0.add("");
dp.add(dp0);
for (int i = 1; i <= n; i++) {
List<String> current = new ArrayList<>();
for (int j = 0; j < i; j++) {
List<String> leftList = dp.get(j);
List<String> rightList = dp.get(i - 1 - j);
for (String left : leftList) {
for (String right : rightList) {
current.add("(" + left + ")" + right);
}
}
}
dp.add(current);
}
return dp.get(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: 不同的二叉搜索树