批量生成 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>
17 KiB
17 KiB
括号生成 (Generate Parentheses)
题目描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
约束条件
1 <= n <= 8
解题思路
方法一:回溯法(推荐)
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性:
- 左括号数量不能超过 n
- 右括号数量不能超过左括号数量
算法步骤:
- 初始化结果数组
result和当前字符串current - 定义回溯函数
backtrack(open, close):open:已使用的左括号数量close:已使用的右括号数量
- 终止条件:
len(current) == 2 * n,将current加入result - 选择条件:
- 如果
open < n,可以添加左括号 - 如果
close < open,可以添加右括号
- 如果
- 递归调用后撤销选择(回溯)
为什么这样做?
- 通过限制
close < open,保证任何时候右括号数量不超过左括号数量 - 通过限制
open < n,保证左括号数量不超过 n - 这样生成的所有组合都是有效的
方法二:DFS 深度优先搜索
**核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。
算法步骤:
- 构建一个递归树,每个节点代表一个状态
- 从根节点开始,每次可以选择添加左括号或右括号
- 剪枝:不符合条件的分支直接跳过
- 到达叶子节点(长度为 2n)时,记录结果
方法三:动态规划
**核心思想:**利用卡特兰数(Catalan Number)的性质。n 对括号的有效组合数等于第 n 个卡特兰数。
递推公式:
dp[n]表示 n 对括号的所有有效组合dp[n] = "(" + dp[i] + ")" + dp[n-1-i],其中i从 0 到 n-1
算法步骤:
- 初始化
dp[0] = [""] - 对于
i从 1 到 n:- 对于
j从 0 到 i-1:- 将
dp[j]的每个组合加上一对括号,再拼接dp[i-1-j]的每个组合
- 将
- 对于
- 返回
dp[n]
代码实现
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 实现(回溯法)
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 实现(动态规划)
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 实现(动态规划)
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: 使用栈或者计数器。
// 方法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: 需要更复杂的逻辑来保证括号匹配。
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: 可以使用生成器模式,逐个生成结果而不是全部存储。
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, ...
应用场景:
- 括号匹配问题(本题)
- 二叉搜索树的计数
- 出栈序列的计数
- 路径计数(不穿过对角线)
计算卡特兰数:
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. 实战扩展:通用回溯框架
回溯法通用模板:
func backtrack(路径, 选择列表) {
if 满足结束条件 {
result = append(result, 路径)
return
}
for 选择 in 选择列表 {
// 做选择
路径.add(选择)
// 递归
backtrack(路径, 选择列表)
// 撤销选择(回溯)
路径.remove(选择)
}
}
应用示例:排列问题
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: 给定一个只包含 '(' 和 ')' 的字符串,找出最长有效(正确闭合)括号子串的长度。
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 互不相同的二叉搜索树有多少种?
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 是平衡括号字符串
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:剪枝优化
在回溯过程中,尽早发现不可能的解并剪枝。
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:迭代优化
使用迭代代替递归,避免栈溢出。
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. 面试技巧
面试官可能会问:
- "为什么要用回溯法而不是暴力枚举?"
- "卡特兰数和这个问题有什么关系?"
- "如何证明你的算法生成的所有组合都是有效的?"
回答要点:
- 回溯法通过剪枝避免了无效组合的生成,效率更高
- n 对括号的有效组合数等于第 n 个卡特兰数
- 通过维护
open和close计数器,保证了右括号永远不超过左括号
7. 相关题目推荐
- LeetCode 22: 括号生成(本题)
- LeetCode 17: 电话号码的字母组合
- LeetCode 32: 最长有效括号
- LeetCode 39: 组合总和
- LeetCode 46: 全排列
- LeetCode 78: 子集
- LeetCode 96: 不同的二叉搜索树