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

17 KiB
Raw Blame History

括号生成 (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 实现(回溯法)

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, ...

应用场景:

  1. 括号匹配问题(本题)
  2. 二叉搜索树的计数
  3. 出栈序列的计数
  4. 路径计数(不穿过对角线)

计算卡特兰数:

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 分
  • ABA + 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. 面试技巧

面试官可能会问:

  1. "为什么要用回溯法而不是暴力枚举?"
  2. "卡特兰数和这个问题有什么关系?"
  3. "如何证明你的算法生成的所有组合都是有效的?"

回答要点:

  1. 回溯法通过剪枝避免了无效组合的生成,效率更高
  2. n 对括号的有效组合数等于第 n 个卡特兰数
  3. 通过维护 openclose 计数器,保证了右括号永远不超过左括号

7. 相关题目推荐

  • LeetCode 22: 括号生成(本题)
  • LeetCode 17: 电话号码的字母组合
  • LeetCode 32: 最长有效括号
  • LeetCode 39: 组合总和
  • LeetCode 46: 全排列
  • LeetCode 78: 子集
  • LeetCode 96: 不同的二叉搜索树