22. 括号生成

中等 含交互动画

学完这道你应该能: 说清它为什么用「括号」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

AI 私教 · 就着本题动画讲
卡住了不用硬扛,按 8 步把这题真正走通。
从读题、试解、找法到手写代码,边问边打勾,适合第一次系统学算法的同学。

题目描述

给 n 对括号,生成所有合法的括号组合(每个左括号都能正确配对)。

n = 3
输出 = ((())) (()()) (())() ()(()) ()()()

思路解析

把所有 ( 和 ) 的排列都生成出来(n=3 就是 2^6=64 个)再挑合法的,绝大多数都是 "())( " 这种废串,白生成。

转折:把「合法」翻译成「什么时候能往下放」。构建过程中只要发现这一步会让前缀非法,立刻掐断这条分支,连试都不试,省掉所有废串。

两条规则:左括号还有剩(left 大于 0)就能放 (;已放的右括号比左括号少(right 大于 left 才有剩),才能放 )。守住这两条,生成的一定合法。

放第 1 个:(:开局只能放 ((一个右括号都没法配对,放 ) 会立刻右多于左)。路径变成 "(",左括号还剩 2 个,右括号此刻不能放。

连放左括号到底:沿「优先放 (」这条分支一路放,把 3 个左括号都放完,路径 "((("。左括号用光(left=0),接下来只能放右括号。

补齐右括号 · 收第 1 条:此时 right(3) 大于 left(0),能连放 3 个右括号,得到 "((()))"。长度到 2n=6、左右配平 → 收进结果。这条分支到头,往回退。

回溯一步 · 试 close 剪枝:回退到只放了一个 ( 的地方,改走「先放 )」:path="()",此时 open=1、close=1。若再放 ),会变成 close 大于 open(右多于左) → 负例分支,剪枝不展开,连递归都不进。

该分支只能放 ( · 续 "(()":所以 "()" 后面只能放 (,得到 "(()"。左括号还剩 1、右括号还差 2,继续往下放就能凑出以 "()" 开头的几个解。

续放右括号 · 收 "()(())":沿 "(()" 这条把剩下的 (、) 按规则补满,凑出 "()(())",长度到 6 → 收下。中途收到的 "(()())"、"(())()" 也是这套放-收流程产出的。

回到底层 · 换最后一条:继续回溯,在 "()" 之后走「(后接)」这一路:path="()()",open=close=2,再放 ( 放 ) 补满 → "()()()"。每一步都靠两条规则把住,不会拐进废串。

所有分支走完:这样不断「放-收-回退-换-该剪就剪」,把每条合法路径都走一遍,废分支一律掐死,最终凑齐全部 5 种

回溯类题的通用思路:把「什么时候合法」翻译成「什么时候能往下放」,不合法的分支直接不进,效率天差地别。

参考代码(Python)

Python
1def generateParenthesis(n):
2    res = []
3    def bt(path, left, right):        # left/right = 还能放几个
4        if len(path) == 2 * n:
5            res.append(class="cl-str">"".join(path)); return
6        if left > 0:                  # 左括号有剩,能放 (
7            bt(path + [class="cl-str">"("], left - 1, right)
8        if right > left:              # 右比左多得剩,能放 )
9            bt(path + [class="cl-str">")"], left, right - 1)
10    bt([], n, n)
11    return res

复杂度分析

  • 时间复杂度:第 n 个卡特兰数 —— 合法串约 4ⁿ/(n√n) 个,每个拼接花 O(n),靠剪枝只走合法分支
  • 空间复杂度:O(n) —— 递归深度最多 2n,path 也最多 2n 长

套路模板

所有回溯都是这个骨架:到终点就收、否则枚举合法选择、选了再撤。本题用「left/right 计数」充当那个「状态」,用计数条件做剪枝。

Python
1def bt(path, 状态):
2    if 到达终点:
3        res.append(path 的快照); return
4    for 每个合法选择:
5        做选择(更新 path 和状态)
6        bt(path, 新状态)
7        撤销选择(回溯)

易错点

  • 错误写法:放 ) 的条件写成 right > 0正确写法:right > left(右括号必须比左括号「少」才能再放 )。写成 right>0 时,比如 path="(" 也会去放 ),凑出 "()" 没问题,但 path=")" 这种会冒出来,导致 )( 这种配不上对的串)
  • 错误写法:收结果时直接存 path 引用正确写法:存 "".join(path) 或 path[:] 的快照(path 后面还会被 pop/修改,存引用会让结果列表里的每一项都变成最后那条)
  • 错误写法:用 path + ["("] 之外还手动回溯正确写法:二选一:要么传新列表、要么 append 后 pop(混用会重复撤销,路径乱套)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →77. 组合 ← 返回题库