在LeetCode的字符串类题目中,「括号生成」绝对是回溯算法的经典入门题——它不仅考察对括号有效性的判断,更考验如何通过递归回溯,高效枚举所有合法组合,避免无效枚举带来的冗余计算。今天就来拆解这道题,从思路分析到代码实现,再到细节优化,带你彻底搞懂这道高频面试题。
一、题目回顾:明确需求与核心难点
题目描述:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3 → 输出:["((()))","(()())","(())()","()(())","()()()"]
输入:n = 1 → 输出:["()"]
核心难点拆解:
-
有效性约束:括号必须成对出现,且左括号必须在右括号之前(比如 "())(" 是无效的);
-
枚举完整性:要列出所有合法组合,不能遗漏,也不能出现重复;
-
效率优化:避免生成无效组合(比如左括号数量超过n、右括号数量超过左括号),减少不必要的递归。
二、思路分析:为什么用DFS回溯?
括号生成的过程,本质上是「逐步构建字符串」的过程——每一步都有两个选择:加左括号 ( 或加右括号 )。但这两个选择并非无限制,必须满足两个核心条件,才能保证生成的括号有效:
-
左括号数量不能超过n(因为总共只有n对括号,左括号最多n个);
-
右括号数量不能超过当前左括号数量(否则会出现 "())" 这种无效情况)。
基于这两个条件,我们可以用「深度优先搜索(DFS)」+「回溯」的思路来解题:
-
定义递归函数,参数记录当前左括号数量、括号平衡度(左括号数 - 右括号数)、当前构建的字符串;
-
递归终止条件:当左括号数量等于n,且平衡度为0(说明左、右括号数量相等,且都达到n个),将当前字符串加入结果集;
-
递归分支:
-
如果左括号数量小于n,可添加左括号,递归时左括号数+1、平衡度+1;
-
如果平衡度大于0(说明当前左括号数大于右括号数),可添加右括号,递归时平衡度-1。
-
这里的「回溯」体现在:递归结束后,会自动回到上一步,尝试另一种选择(比如先加左括号,递归返回后,再尝试加右括号),无需手动回退字符串(因为字符串是值类型,递归传递时会创建新的副本,不影响上一层的状态)。
三、代码实现与逐行解析
先给出完整代码(TypeScript版本,与题目中给出的代码一致,重点解析核心逻辑):
function generateParenthesis(n: number): string[] {
// 存储最终结果的数组
const res: string[] = [];
// 递归函数(DFS)
// openCount:当前已使用的左括号数量
// balance:括号平衡度(左括号数 - 右括号数)
// currentStr:当前正在构建的括号字符串
const dfs = (openCount: number, balance: number, currentStr: string) => {
// 终止条件:左括号用完(等于n),且平衡度为0(左右括号相等)
if (openCount === n && balance === 0) {
res.push(currentStr);
return;
}
// 分支1:添加左括号(前提是左括号还没到n个)
if (openCount < n) {
dfs(openCount + 1, balance + 1, currentStr + '(');
}
// 分支2:添加右括号(前提是当前左括号数 > 右括号数,即balance > 0)
if (balance > 0) {
dfs(openCount, balance - 1, currentStr + ')');
}
}
// 初始调用:左括号数0,平衡度0,空字符串
dfs(0, 0, '');
return res;
};
逐行解析核心细节
-
结果数组 res:用于存储所有合法的括号组合,递归终止时将符合条件的字符串加入。
-
递归函数 dfs 的三个参数:
-
openCount:记录当前已经使用了多少个左括号,用于限制左括号总数不超过n;
-
balance:平衡度,核心作用是判断是否能添加右括号——balance = 左括号数 - 右括号数,只有balance > 0时,添加右括号才不会导致无效;
-
currentStr:当前正在构建的字符串,每递归一次,就拼接一个括号。
-
-
终止条件:当 openCount === n(左括号用完)且 balance === 0(右括号数等于左括号数),说明当前字符串是合法的,加入res。
-
两个递归分支:
-
添加左括号:只有 openCount< n 时才能执行,此时左括号数+1,平衡度+1(因为多了一个左括号);
-
添加右括号:只有 balance > 0 时才能执行,此时平衡度-1(因为多了一个右括号),openCount 不变(右括号不影响左括号数量)。
-
-
初始调用:从空字符串开始,左括号数0,平衡度0,启动递归。
四、优化点与拓展思考
1. 为什么这个算法高效?
相比「暴力枚举所有可能的2n个括号组合,再判断有效性」的方法,该算法的时间复杂度是 O(4ⁿ/√n)(卡特兰数复杂度),空间复杂度是 O(n)(递归栈深度为2n,结果数组存储卡特兰数个数的元素)。
核心优势:通过两个条件(openCount < n、balance > 0),直接过滤掉所有无效组合,避免了无效枚举和有效性判断的冗余计算,效率大幅提升。
2. 其他实现方式?
除了DFS回溯,还可以用BFS(广度优先搜索)实现——每一层存储当前构建的字符串、左括号数量、平衡度,依次枚举每一步的选择,直到达到终止条件。两种方法思路一致,只是遍历方式不同,DFS更简洁,BFS更直观。
3. 面试延伸问题
这道题常被面试官追问的问题:
-
如何证明生成的组合没有重复?(因为每一步的选择都是明确的,左括号优先于右括号的合理分支,不会出现重复组合);
-
如果n很大(比如n=1000),会出现什么问题?(递归栈溢出,此时需要用迭代版DFS或BFS,避免栈溢出);
-
卡特兰数与这道题的关系?(n对括号的合法组合数,正是第n个卡特兰数,公式为 C(2n, n)/(n+1))。
五、总结
LeetCode 22题括号生成,核心是「用DFS回溯+有效性剪枝」,抓住两个关键约束(左括号不超过n、右括号不超过左括号),就能高效生成所有合法组合。
这道题的价值不仅在于解题本身,更在于掌握「回溯算法的剪枝思想」——在枚举过程中,提前过滤无效路径,减少冗余计算,这是很多回溯题(如子集、排列)的核心解题思路。