跳到主要内容

22. 括号生成

题目描述

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

示例:

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

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

约束:

  • 1 <= n <= 8

解题思路

第一步:理解问题本质

有效括号组合的两个核心约束:

  1. 左括号总数 = 右括号总数 = n
  2. 在字符串的任意前缀中,左括号数量必须 >= 右括号数量(否则出现多余的右括号,无法匹配)

这两个约束决定了:在构建过程中的任意时刻,

  • 已用左括号数 left < n 时,才能继续放左括号
  • 已用右括号数 right < 已用左括号数 left,才能放右括号(确保右括号不超过左括号)

当右括号数量达到 n 时,说明整个字符串已经填满(共 2n 个字符),且合法,加入答案。

第二步:暴力解法

思路: 生成所有长度为 2n 的括号字符串,然后逐一验证是否合法。

class Solution:
def generateParenthesis(self, n: int):
def is_valid(s):
count = 0
for c in s:
if c == '(':
count += 1
else:
count -= 1
if count < 0:
return False
return count == 0

ans = []
# 生成所有 2^(2n) 种组合
for bits in range(1 << (2 * n)):
s = ''
for i in range(2 * n):
s += '(' if (bits >> i) & 1 else ')'
if is_valid(s):
ans.append(s)
return ans

为什么不够好:

  • 时间复杂度 O(2^(2n) * n):枚举了所有 2^(2n) 种字符串,绝大多数都是非法的,严重浪费
  • 对于 n=8,需要枚举 2^16 = 65536 种字符串,但合法的只有 1430 种(第 8 个卡特兰数)

第三步:优化思路

暴力法的问题在于:在构建过程中,即使已经发现当前路径不可能产生合法括号(比如右括号已经多于左括号),仍然继续枚举。

改进方向:边构建边剪枝。一旦发现当前状态违反约束,立刻停止,不再向下搜索。这正是回溯(DFS + 剪枝)的核心思想。

第四步:最优解法(回溯 DFS)

状态定义:

  • left:已放入的左括号数量
  • right:已放入的右括号数量
  • 当前位置下标 = left + right(已放字符总数)

剪枝条件(合法性约束):

  • 只有 left < n 时,才可以放左括号
  • 只有 right < left 时,才可以放右括号

终止条件:

  • right == n,说明已放了 n 个右括号(同时也有 n 个左括号),字符串构建完毕,加入答案

用预分配数组而非字符串拼接: 字符串每次拼接会产生新对象,而 path = [''] * (2n) 预先分配好,每次直接修改对应位置,最后一次性 join,效率更高。


完整代码实现

from typing import List

class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
path = [''] * (n * 2) # 预分配长度为 2n 的数组

def dfs(left: int, right: int) -> None:
# 终止:已放 n 个右括号,字符串构建完毕
if right == n:
ans.append(''.join(path))
return
# 放左括号:还有剩余配额
if left < n:
path[left + right] = '(' # 当前位置 = 已放字符总数
dfs(left + 1, right)
# 放右括号:右括号数量未超过左括号
if right < left:
path[left + right] = ')'
dfs(left, right + 1)

dfs(0, 0)
return ans

示例推演

n = 2 为例,展示完整的 DFS 搜索树(left 为已放左括号数,right 为已放右括号数):

dfs(0, 0)  path=["",""]  当前位置 0
├── 放 '(' → dfs(1, 0) path=["(",""] 当前位置 1
│ ├── 放 '(' → dfs(2, 0) path=["(","("] 当前位置 2
│ │ └── 放 ')'(right=0 < left=2)→ dfs(2, 1) path=["(","(","",")",...] 当前位置 3
│ │ └── 放 ')'(right=1 < left=2)→ dfs(2, 2) path=["(","(",")",")"] 当前位置 4
│ │ └── right==n==2,加入 "(())" ✓
│ └── 放 ')'(right=0 < left=1)→ dfs(1, 1) path=["(",")",...] 当前位置 2
│ └── 放 '('(left=1 < n=2)→ dfs(2, 1) path=["(","(",")",...) ... 当前位置 3
│ └── 放 ')'(right=1 < left=2)→ dfs(2, 2) path=["(",")",","(",")",...]
│ └── right==n==2,加入 "()()" ✓

最终答案:["(())", "()()"],共 2 种,符合预期(卡特兰数 C_2 = 2)。

逐步追踪第一条路径 "(())" 的 path 数组变化:

步骤操作leftright当前位置 left+rightpath
初始-00-['','','','']
1(100['(','','','']
2(201['(','(','','']
3)212['(','(',')', '']
4)223['(','(',')',')']
5right==2==n---加入 "(())"

复杂度分析

解法时间复杂度空间复杂度说明
暴力枚举O(2^(2n) · n)O(n)枚举所有字符串再验证,大量无效枚举
回溯 DFS(最优)O(C_n · n)O(n)C_n 为第 n 个卡特兰数,仅枚举合法路径

卡特兰数 C_n 的近似值为 O(4^n / n^(3/2)),远小于 2^(2n) = 4^n。回溯通过剪枝,只访问合法状态,效率大幅提升。

空间复杂度 O(n):递归栈深度最大为 2n(每次放一个字符),path 数组长度为 2n。


易错点总结

  • 终止条件用 right == n 而非 left == n 必须等右括号也全部放完,字符串才是完整合法的。若只检查 left == n,此时可能右括号还没放完。
  • 当前位置下标是 left + right 已放的字符总数恰好是 left + right,这是预分配 path 数组的关键索引。
  • 两个 if 是独立的,不是 if-else: 在同一状态下,放左括号和放右括号是两个独立的分支,都需要尝试(只要满足各自条件)。
  • path 复用无需回溯清除: 由于 path[left + right] = ... 总是写入确定的位置(由当前的 left 和 right 唯一确定),下一次写入会覆盖该位置,无需手动清除。

扩展思考

  • 卡特兰数(Catalan Number): n 对括号的合法组合数恰好是第 n 个卡特兰数 C_n = C(2n,n)/(n+1)。卡特兰数在算法中非常常见,如二叉搜索树的结构数、三角剖分方案数等。
  • 动态规划解法: dp[n] 可以由 dp[i]dp[n-1-i](i 从 0 到 n-1)组合得到,含义是第一对括号内有 i 对,括号外右侧有 n-1-i 对。
  • 相关题目: LeetCode 20(有效的括号)用栈验证括号合法性,是本题合法性判断的逆问题;LeetCode 32(最长有效括号)在有效括号的基础上求最长子串。
加载评论中...