想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第63课:括号生成
模块:回溯算法 | 难度:Medium ⭐⭐⭐ LeetCode 链接:leetcode.cn/problems/ge… 前置知识:第59课(全排列) 预计学习时间:25分钟
🎯 题目描述
给定一个整数 n,生成所有合法的括号组合。也就是说,有 n 对括号,要生成所有可能的、括号正确配对的字符串。
示例:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
解释:有 3 对括号,所有合法的组合方式
输入:n = 1
输出:["()"]
约束条件:
1 <= n <= 8- 必须保证括号的有效性:左括号数量始终 >= 右括号数量
- 每个结果字符串长度为
2*n
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 最小输入 | n=1 | ["()"] | 基本功能 |
| 中等规模 | n=2 | ["(())","()()"] | 递归正确性 |
| 典型输入 | n=3 | 5种组合 | 完整性 |
| 上界 | n=8 | 1430种组合 | 性能边界(卡塔兰数) |
💡 思路引导
生活化比喻
想象你在排队进出一个礼堂:
🚪 规则:每个人进去时拿一张票(左括号),出来时交还一张票(右括号)。礼堂容量有限(n对括号),关键约束是:任何时刻,出来的人不能比进去的人多(否则就没票可交了!)
🐌 笨办法:生成所有 2n 个位置的排列(如2^(2n)种),然后逐一检查是否合法。太慢!
🚀 聪明办法:边生成边检查!每次只在"合法"的时候添加括号:
- 左括号还有剩余 → 可以加"("
- 右括号数量 < 左括号数量 → 可以加")" 这样生成的每一个字符串都是合法的,不需要事后过滤!
关键洞察
**核心约束:**在构造过程中,任何时刻"已使用的右括号数"必须 ≤ "已使用的左括号数",这样才能保证括号合法配对。
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:整数 n (1~8),表示有 n 对括号
- 输出:返回所有合法的括号字符串列表
- 限制:字符串长度固定为 2n,必须保证括号配对合法
Step 2:先想笨办法(暴力法)
生成所有长度为 2n 的字符串(每个位置可以是"("或")",共 2^(2n) 种),然后逐个检查是否合法。
- 时间复杂度:O(2^(2n) * n) — 生成+验证
- 瓶颈在哪:生成了大量不合法的字符串,浪费了计算
Step 3:瓶颈分析 → 优化方向
笨办法的问题是"先生成所有可能,再过滤"。能不能边生成边剪枝,只构造合法的字符串?
- 核心问题:如何保证生成过程中不产生非法字符串
- 优化思路:约束回溯 — 每次添加括号时检查约束条件,不满足就不添加
Step 4:选择武器
- 选用:回溯算法 + 约束剪枝
- 理由:回溯天然适合"生成所有可能的组合",加上约束条件可以提前剪枝,避免生成无效字符串
🔑 模式识别提示:当题目要求"生成所有满足约束条件的组合",优先考虑"约束回溯"
🔑 解法一:暴力生成+验证(朴素法)
思路
先生成所有可能的括号序列,再逐一检查是否合法。用回溯生成所有 2^(2n) 种可能,然后用栈验证括号配对。
图解过程
以 n=2 为例,生成所有长度为 4 的括号串:
决策树(部分):
""
/ \
"(" ")" <- 不合法起点
/ \
"((" "()"
/ \ / \
"(((" "(()" "()(" "())"
... ... ... ...
生成所有 2^4=16 种,再检查:
- "(())" ✓ 合法
- "()()" ✓ 合法
- "())(" ✗ 不合法
- "))((" ✗ 不合法
...
Python代码
from typing import List
def generateParenthesis_brute(n: int) -> List[str]:
"""
解法一:暴力生成+验证
思路:生成所有可能的括号序列,再检查合法性
"""
def is_valid(s: str) -> bool:
"""用栈验证括号是否合法"""
balance = 0
for ch in s:
if ch == '(':
balance += 1
else:
balance -= 1
if balance < 0: # 右括号多了
return False
return balance == 0 # 最终必须配对完
def backtrack(path: str):
"""生成所有长度为 2n 的括号串"""
if len(path) == 2 * n:
if is_valid(path):
result.append(path)
return
backtrack(path + '(') # 试左括号
backtrack(path + ')') # 试右括号
result = []
backtrack('')
return result
# ✅ 测试
print(generateParenthesis_brute(2)) # 期望输出:["(())","()()"]
print(generateParenthesis_brute(3)) # 期望输出:5种组合
复杂度分析
- 时间复杂度:O(2^(2n) * n) — 生成 2^(2n) 个字符串,每个验证需要 O(n)
- 具体地说:如果 n=3,需要生成 2^6=64 个字符串,每个检查需要 6 次操作,共约 384 次操作(实际只有 5 个合法)
- 空间复杂度:O(n) — 递归栈深度
优缺点
- ✅ 思路直接,易于理解
- ❌ 生成大量无效字符串,效率极低(n=8 时要生成 65536 个字符串!)
🏆 解法二:约束回溯(最优解)
优化思路
不生成无效字符串!在构造过程中实时检查约束条件:
- 左括号数量 < n → 可以加"("
- 右括号数量 < 左括号数量 → 可以加")"
这样每次都只走"合法路径",直接生成答案,无需验证。
💡 关键想法:用两个计数器 left 和 right,分别记录已使用的左括号和右括号数量。约束条件
right < left保证了任何时刻"已配对的右括号不会超过左括号",即括号始终合法。
图解过程
以 n=2 为例,约束回溯只走合法路径:
""(left=0,right=0)
|
"("(1,0)
/ \
"((") (2,0) "()") (1,1)
| |
"(()"(2,1) "()("(2,1)
| |
"(())"(2,2)✓ "()()"(2,2)✓
剪枝说明:
- 从""开始,只能加"("(right=0 无法加")")
- 从"("可以加"("或")",两条路都合法
- 从"(("只能加")"(left已达上限2)
- 从"()"可以加"("(right<left)
关键:每个决策点都遵循约束,所以叶子节点一定是合法串!
再看一个 n=3 的完整图解:
n=3 时,部分决策树:
""
|
"("
/ \
"(( " "()
/ \ / \
"(((" "(() "()(" "()(
/ / \ ... ...
"((() "(()"( ...
... ...
最终生成5个合法串:
1. "((()))" - 3层嵌套
2. "(()())" - 2层嵌套+1个并列
3. "(())()" - 1个嵌套+1个独立
4. "()(())" - 1个独立+1个嵌套
5. "()()()" - 3个并列
Python代码
def generateParenthesis(n: int) -> List[str]:
"""
🏆 解法二:约束回溯(最优解)
思路:边生成边约束,只添加合法的括号
"""
result = []
def backtrack(path: str, left: int, right: int):
"""
path: 当前构造的字符串
left: 已使用的左括号数量
right: 已使用的右括号数量
"""
# 终止条件:字符串长度达到 2n
if len(path) == 2 * n:
result.append(path)
return
# 决策1:加左括号(只要还有剩余)
if left < n:
backtrack(path + '(', left + 1, right)
# 决策2:加右括号(只有 right < left 时才合法)
if right < left:
backtrack(path + ')', left, right + 1)
backtrack('', 0, 0)
return result
# ✅ 测试
print(generateParenthesis(1)) # 期望输出:["()"]
print(generateParenthesis(2)) # 期望输出:["(())","()()"]
print(generateParenthesis(3)) # 期望输出:["((()))","(()())","(())()","()(())","()()()"]
复杂度分析
- 时间复杂度:O(4^n / sqrt(n)) — 这是第 n 个卡塔兰数 C_n 的渐近复杂度
- 具体地说:n=3 时只生成 5 个字符串(而非 64 个),n=8 时生成 1430 个(而非 65536 个)
- 精确值:C_n = (2n)! / ((n+1)! * n!),增长速度远低于 2^(2n)
- 空间复杂度:O(n) — 递归栈深度为 2n
为什么是最优解
- ✅ 时间复杂度已达理论最优:必须生成所有合法组合,无法更快
- ✅ 无需额外验证:构造过程保证了合法性
- ✅ 代码简洁:核心只有 2 个 if 判断
🐍 Pythonic 写法
利用生成器节省内存(适合只需要逐个处理结果的场景):
def generateParenthesis_generator(n: int):
"""生成器版本:按需生成,不占用额外列表空间"""
def backtrack(path: str, left: int, right: int):
if len(path) == 2 * n:
yield path
return
if left < n:
yield from backtrack(path + '(', left + 1, right)
if right < left:
yield from backtrack(path + ')', left, right + 1)
return list(backtrack('', 0, 0))
# 使用生成器逐个处理
for parentheses in backtrack('', 0, 0, n=2):
print(parentheses) # 逐个输出,不占用列表空间
⚠️ 面试建议:先写清晰版本展示思路,再提生成器版本展示 Python 功底。面试官更看重你的约束回溯思路,而非语法糖。
📊 解法对比
| 维度 | 解法一:暴力生成+验证 | 🏆 解法二:约束回溯(最优) |
|---|---|---|
| 时间复杂度 | O(2^(2n) * n) | O(4^n / sqrt(n)) ← 最优 |
| 空间复杂度 | O(n) | O(n) |
| 代码难度 | 中等 | 简单 |
| 面试推荐 | ⭐ | ⭐⭐⭐ ← 首选 |
| 适用场景 | 仅适合 n≤3 | 通用,n≤8 完全胜任 |
面试建议:
- 先口述暴力法思路(30秒),说明"生成所有可能再验证"
- 立即优化到🏆约束回溯,强调两个关键约束:
left < n→ 可以加左括号right < left→ 可以加右括号(这是保证合法性的核心!)
- 手动演示 n=2 的递归树,展示剪枝效果
- 强调时间复杂度从指数级 2^(2n) 降到卡塔兰数级别
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你生成所有合法的 n 对括号组合。
你:(审题30秒)好的,这道题要求生成所有合法的括号字符串。合法的意思是括号必须正确配对,不能出现")("这样的情况。
我的第一个想法是用回溯生成所有可能的字符串,然后再验证是否合法,但这样会生成很多无效字符串。
更好的方法是约束回溯:在构造过程中就保证合法性。核心思路是:
- 用两个计数器 left 和 right,分别记录已使用的左右括号数
- 只有 left < n 时才能加"("
- 只有 right < left 时才能加")"(这保证了右括号不会超过左括号)
这样每次都只走合法路径,生成的都是答案,时间复杂度是卡塔兰数级别 O(4^n / sqrt(n))。
面试官:很好,请写一下代码。
你:(边写边说)
def generateParenthesis(n: int) -> List[str]:
result = []
def backtrack(path, left, right):
# 终止条件:凑够 2n 个字符
if len(path) == 2 * n:
result.append(path)
return
# 决策1:加左括号
if left < n:
backtrack(path + '(', left + 1, right)
# 决策2:加右括号(关键约束!)
if right < left:
backtrack(path + ')', left, right + 1)
backtrack('', 0, 0)
return result
面试官:测试一下?
你:用 n=2 测试:
- 初始
('', 0, 0) - 只能加"(",变成
('(', 1, 0) - 可以加"("或")",分两路:
- 路径1:
('((', 2, 0)→ 只能加")" →('(()', 2, 1)→('(())', 2, 2)✓ - 路径2:
('()', 1, 1)→ 只能加"(" →('()(', 2, 1)→('()()', 2, 2)✓
- 路径1:
- 结果:
["(())","()()"]正确!
边界情况 n=1:["()"] 正确!
高频追问
| 追问 | 应答策略 |
|---|---|
| "为什么 right < left 能保证合法?" | 核心原理:任何时刻,已使用的右括号不超过左括号,意味着"每个右括号都能找到前面的左括号配对"。比如"(()":前两个"("可以配对后面两个")",但如果是"())",第3个")"就找不到配对的"("了。 |
| "时间复杂度为什么是卡塔兰数?" | 卡塔兰数是组合数学中的经典数列,第 n 个卡塔兰数 C_n 表示 n 对括号的合法组合数。公式是 C_n = (2n)! / ((n+1)! * n!)。渐近复杂度约为 O(4^n / (n * sqrt(n)))。 |
| "能不能用迭代代替递归?" | 可以用栈模拟递归,但代码会复杂很多。递归版本更清晰,且 n≤8 时递归深度只有 16,不会栈溢出,面试中推荐递归。 |
| "如果要求返回第 k 个组合呢?" | 可以在回溯中加计数器,找到第 k 个就返回。或者用数学方法:卡塔兰数有递推公式,可以直接跳到第 k 个。 |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:字符串拼接在回溯中的使用
path + '(' # 创建新字符串,不影响原 path(自动回溯)
# vs
path.append('('); ...; path.pop() # 列表需要手动回溯
# 技巧2:多返回值简化参数
def backtrack(left, right): # 只传计数器
path = '(' * left + ')' * right # 临时构造字符串
# 缺点:每次都重新构造,效率低
# 技巧3:yield from 递归生成器
yield from backtrack(...) # 递归生成所有结果
💡 底层原理(选读)
卡塔兰数是组合数学中的重要数列,出现在很多问题中:
- 定义:C_0=1, C_{n+1} = Σ(C_i * C_{n-i}), i=0~n
- 通项公式:C_n = (2n)! / ((n+1)! * n!) = C(2n, n) / (n+1)
- 前几项:1, 1, 2, 5, 14, 42, 132, 429, 1430...
- 应用场景:
- n 对括号的合法组合数
- n+1 个叶子的二叉树形态数
- n×n 方格从左下到右上不穿过对角线的路径数
- n 个元素的出栈序列数
为什么括号问题是卡塔兰数? 把"("看作 +1,把")"看作 -1,合法括号序列等价于:
- 前缀和始终 ≥0(right≤left)
- 总和为 0(配对完整) 这正是从 (0,0) 到 (n,n) 的单调路径数,即 C_n!
算法模式卡片 📐
- 模式名称:约束回溯(Backtracking with Constraints)
- 适用条件:需要生成满足特定约束的所有组合/排列
- 识别关键词:"生成所有"+"满足条件"+"括号/路径/配对"
- 模板代码:
def constrained_backtrack(params):
result = []
def backtrack(state, counters):
# 终止条件
if is_complete(state):
result.append(state)
return
# 尝试所有合法决策
for choice in get_valid_choices(counters):
# 做选择(不需要撤销,因为传递的是新状态)
new_state = state + choice
new_counters = update_counters(counters, choice)
backtrack(new_state, new_counters)
backtrack(initial_state, initial_counters)
return result
易错点 ⚠️
-
约束条件弄反:写成
if right < n and left < right→ 错!应该是right < left- 原因:
right < left保证"已用右括号不超过左括号",而left < right会导致"((("这样的串无法生成
- 原因:
-
忘记终止条件:只检查
left == n,不检查right == n→ 可能提前返回- 正确做法:检查
len(path) == 2*n或left == n and right == n
- 正确做法:检查
-
字符串拼接理解错误:以为
path + '('会修改原 path → 不会!Python 字符串不可变,自动实现回溯效果
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
- 场景1:语法解析器:编译器在解析代码时,需要检查括号/大括号/中括号的配对,用的就是"实时计数"思想
- 场景2:前端组件嵌套:React/Vue 中检查组件标签是否正确闭合
<div>...</div>,原理相同 - 场景3:数学表达式生成:AI 系统生成合法的数学公式(如 LaTeX),需要保证括号配对
- 场景4:网络协议验证:检查 JSON/XML 的大括号/标签配对,都用"左右计数器"方法
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 20. 有效的括号 | Easy | 栈验证括号 | 用栈检查括号配对,是本题的"验证"部分 |
| LeetCode 32. 最长有效括号 | Hard | DP/栈 | 找最长的合法子串,可以用"左右计数"思想 |
| LeetCode 301. 删除无效括号 | Hard | BFS/回溯 | 删除最少字符使括号合法,类似"修复"版本 |
| LeetCode 241. 为运算表达式设计优先级 | Medium | 分治回溯 | 给表达式加括号改变运算顺序,本质是括号插入 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:给定 n 对括号,生成所有合法的括号组合,但要求输出按字典序排序。(例如 n=2 时,输出 ["(())","()()"] 而非乱序)
💡 提示(实在想不出来再点开)
回溯的递归顺序天然保证字典序!因为我们先递归 path + '(',再递归 path + ')',所以生成顺序就是字典序。
✅ 参考答案
def generateParenthesis_sorted(n: int) -> List[str]:
"""
生成按字典序排序的括号组合
核心:回溯的递归顺序天然保证字典序
"""
result = []
def backtrack(path: str, left: int, right: int):
if len(path) == 2 * n:
result.append(path)
return
# 先递归'(',再递归')' → 保证字典序
if left < n:
backtrack(path + '(', left + 1, right)
if right < left:
backtrack(path + ')', left, right + 1)
backtrack('', 0, 0)
return result # 无需额外排序!
# 测试
print(generateParenthesis_sorted(3))
# 输出:['((()))', '(()())', '(())()', '()(())', '()()()']
# 已经是字典序!
解释:回溯算法的递归顺序决定了生成顺序。因为我们总是"优先尝试左括号",所以生成的第一个结果一定是 "(((...)))"(全嵌套),最后一个结果是 "()()...()"(全并列)。这正好是字典序!
扩展:如果要逆字典序,只需交换两个 if 的顺序,先递归 ')' 再递归 '(' 即可。
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。