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

24 KiB
Raw Blame History

括号生成 (Generate Parentheses)

题目描述

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例

示例 1

输入n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2

输入n = 1
输出:["()"]

约束条件

  • 1 <= n <= 8

思路推导

暴力解法分析

第一步:最直观的思路 - 生成所有可能的组合

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. 只生成有可能合法的组合
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理解有效括号的生成规则

# 规则1左括号数量不能超过 n
if open < n:
    可以添加 '('

# 规则2右括号数量不能超过左括号数量
if close < open:
    可以添加 ')'

Q: 为什么右括号数量不能超过左括号数量?

A: 因为在任何前缀中,如果右括号多于左括号,就不可能通过后续添加括号使其变成有效括号。

举例:

")" → 不可能变有效,因为第一个字符就是右括号
"())(" → 中间的 "()" 后面是 ")",右括号已经多余了

步骤2设计回溯函数

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。每次递归调用都是独立的,不需要手动撤销。

如果要使用列表优化性能:

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初始化并启动回溯

result = []
backtrack("", 0, 0)
return result

Q: 为什么初始状态是 open=0, close=0

A: 因为我们从空字符串开始,还没有添加任何括号。

关键细节说明

细节1为什么终止条件是 len(current) == 2 * n

# 错误理解:应该是 len(current) == n
# 错误原因n 是括号的对数,每对有 2 个括号

# 正确理解:总长度是 2n
# 例如 n=3最终字符串长度是 6"((()))"

细节2为什么两个 if 是独立的,而不是 if-else

# 错误写法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

# 错误写法
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 实现(回溯法)

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]
}

执行过程演示

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忘记剪枝条件

错误写法:

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
}

正确写法:

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

错误写法:

if open < n {
    backtrack(open+1, close)
} else if close < open {
    backtrack(open, close+1)
}

正确写法:

if open < n {
    backtrack(open+1, close)
}
if close < open {
    backtrack(open, close+1)
}

**原因:**两个条件可能同时满足,需要都尝试。

错误3终止条件错误

错误写法:

if open == n && close == n {  // 错误
    result = append(result, string(current))
    return
}

正确写法:

if len(current) == 2*n {  // 正确
    result = append(result, string(current))
    return
}

**原因:**虽然 open==n && close==n 等价于 len(current)==2*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: 不同的二叉搜索树