📌 题目链接:22. 括号生成 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:字符串、回溯、递归、卡特兰数
⏱️ 目标时间复杂度:O(4ⁿ / √n) (第 n 个卡特兰数的渐近上界)
💾 空间复杂度:O(n) (递归栈深度)
🧠 题目分析
本题要求生成 所有有效的括号组合,其中“有效”指的是:
- 每个左括号
'('都有对应的右括号')'; - 在任何前缀中,右括号的数量不能超过左括号的数量(即不能出现
")("这样的非法前缀)。
输入为一个整数 n(1 ≤ n ≤ 8),表示要生成 n 对括号,即最终每个字符串长度为 2n。
例如,当 n = 3 时,合法组合有 5 种,这恰好是 第 3 个卡特兰数 C₃ = 5。
💡 卡特兰数(Catalan Number)小知识:
第 n 个卡特兰数公式为:它广泛应用于:合法括号序列、二叉树结构计数、出栈序列、不交叉连线等问题。
在本题中,有效括号组合的总数 = Cₙ。
🔁 核心算法及代码讲解 —— 回溯法(Backtracking)
虽然暴力法可以生成所有 2^(2n) 种可能再验证,但效率极低(指数爆炸)。回溯法通过剪枝提前排除非法路径,是本题最优解法。
✅ 回溯的核心思想:
我们维护两个计数器:
open:已使用的左括号数量;close:已使用的右括号数量。
在每一步决策中,遵循以下规则:
- 若
open < n→ 可以添加'('(因为最多只能有 n 个左括号); - 若
close < open→ 可以添加')'(右括号不能超过左括号,否则非法)。
这样,每一步都保证当前字符串前缀是“潜在合法”的,从而避免无效搜索。
🧩 为什么这样能覆盖所有解?
- 所有合法序列必然以
'('开头; - 每次添加括号都满足“左 ≥ 右”且“左 ≤ n”,因此最终一定能构造出所有 Cₙ 种组合;
- 递归到
cur.size() == 2n时,open == close == n,自然合法,无需额外验证。
💻 C++ 代码详解(带行注释)
void backtrack(vector<string>& ans, string& cur, int open, // 已用左括号数
int close, // 已用右括号数
int n) { // 总对数
// 🎯 终止条件:字符串长度达到 2n
if (cur.size() == n * 2) {
ans.push_back(cur); // 当前组合合法,加入结果集
return;
}
// ✅ 剪枝1:左括号未用完,可加 '('
if (open < n) {
cur.push_back('(');
backtrack(ans, cur, open + 1, close, n); // 递归
cur.pop_back(); // 回溯:恢复现场
}
// ✅ 剪枝2:右括号少于左括号,可加 ')'
if (close < open) {
cur.push_back(')');
backtrack(ans, cur, open, close + 1, n);
cur.pop_back(); // 回溯
}
}
🔍 面试高频考点:
- 回溯三要素:选择(add)、递归、撤销(pop);
- 剪枝条件设计:如何保证每一步都合法?——这是回溯优化的关键;
- 空间复用:使用引用
string& cur避免频繁拷贝,提升性能。
🧭 解题思路(分步拆解)
-
初始化:创建空结果列表
result和当前路径字符串current; -
启动回溯:从
open = 0,close = 0开始; -
递归构建:
- 若还能加左括号(
open < n),就加'('并递归; - 若右括号数量小于左括号(
close < open),就加')'并递归;
- 若还能加左括号(
-
到达叶子节点:当字符串长度为
2n,说明已形成完整合法序列,加入结果; -
回溯恢复:每次递归返回后,弹出最后添加的字符,尝试其他分支。
🌟 关键洞察:
不需要检查整个字符串是否合法!通过构造过程保证合法性,这是回溯优于暴力法的根本原因。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(4ⁿ / √n) —— 即第 n 个卡特兰数的渐近上界。每个合法序列需 O(n) 时间复制到结果数组。 |
| 空间复杂度 | O(n) —— 递归栈最大深度为 2n,每层 O(1) 空间;结果数组不计入额外空间。 |
| 剪枝效果 | 暴力法需遍历 2^(2n) = 4ⁿ 种可能,而回溯只遍历 Cₙ ≈ 4ⁿ / (n√n) 种,指数级优化! |
💬 面试官可能会问:
- “为什么不用 BFS?” → 回溯天然适合构造型问题,BFS 需要存储大量中间状态,内存开销大。
- “能否用动态规划?” → 可以(如方法三的分治+记忆化),但回溯更直观、代码更简洁。
- “n=8 时有多少种?” → C₈ = 1430,可快速回答体现数学素养。
💻 完整代码
✅ C++ 实现
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
void backtrack(vector<string>& ans, string& cur, int open, int close, int n) {
if (cur.size() == n * 2) {
ans.push_back(cur);
return;
}
if (open < n) {
cur.push_back('(');
backtrack(ans, cur, open + 1, close, n);
cur.pop_back();
}
if (close < open) {
cur.push_back(')');
backtrack(ans, cur, open, close + 1, n);
cur.pop_back();
}
}
vector<string> generateParenthesis(int n) {
vector<string> result;
string current;
backtrack(result, current, 0, 0, n);
return result;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 🧪 测试用例 1
int n1 = 3;
auto res1 = sol.generateParenthesis(n1);
cout << "Input: n = " << n1 << "\n";
cout << "Output: [";
for (int i = 0; i < res1.size(); ++i) {
cout << """ << res1[i] << """;
if (i != res1.size() - 1) cout << ",";
}
cout << "]\n\n";
// 🧪 测试用例 2
int n2 = 1;
auto res2 = sol.generateParenthesis(n2);
cout << "Input: n = " << n2 << "\n";
cout << "Output: ["" << res2[0] << ""]\n";
return 0;
}
✅ JavaScript 实现
/**
* @param {number} n
* @return {string[]}
*/
var generateParenthesis = function(n) {
const result = [];
function backtrack(cur, open, close) {
// 终止条件
if (cur.length === 2 * n) {
result.push(cur);
return;
}
// 尝试加左括号
if (open < n) {
backtrack(cur + '(', open + 1, close);
}
// 尝试加右括号(必须满足 close < open)
if (close < open) {
backtrack(cur + ')', open, close + 1);
}
}
backtrack('', 0, 0);
return result;
};
// 🧪 测试
console.log("Input: n = 3");
console.log("Output:", generateParenthesis(3));
console.log("Input: n = 1");
console.log("Output:", generateParenthesis(1));
💡 JS 版本使用字符串拼接(不可变),无需手动回溯,但空间开销略大;C++ 使用可变字符串 + 引用,更高效。
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!