
本文深入解析生成 n 对合法括号的所有组合的回溯算法,重点阐明递归调用栈如何在首次 res.push() 后继续回溯探索其他路径,从而完整覆盖所有有效解。
本文深入解析生成 n 对合法括号的所有组合的回溯算法,重点阐明递归调用栈如何在首次 `res.push()` 后继续回溯探索其他路径,从而完整覆盖所有有效解。
生成合法括号序列是回溯(Backtracking)与深度优先搜索(DFS)的经典范例。其核心不在于“一次性走到底”,而在于递归返回后恢复现场、尝试其他分支——这正是理解 findCombo 为何能生成多个结果的关键。
我们以 n = 2 为例,逐步展开调用过程:
初始调用:findCombo(2, "", 0, 0)
→ 满足 usedOpen < 2,进入左分支:findCombo(2, "(", 1, 0)第二层:findCombo(2, "(", 1, 0)
→ 仍可加 '(':findCombo(2, "((", 2, 0)
→ 此时 usedOpen === 2,不能再加 '(';但 usedOpen > usedClose(2 > 0),可加 ')':findCombo(2, "(()", 2, 1)第三层:findCombo(2, "(()", 2, 1)
→ usedOpen === n,不能加 '(';但 2 > 1 且 usedClose < 2,可加 ')':findCombo(2, "(())", 2, 2)第四层:findCombo(2, "(())", 2, 2)
→ 终止条件触发:usedOpen === n && usedClose === n → res.push("(())")
→ 当前递归函数执行完毕,返回至上一层(即第三层)
⚠️ 关键点来了:第三层函数在 findCombo(2, "(())", 2, 2) 返回后,自身函数体已执行完毕,自然退出,控制权回到第二层 —— 而第二层尚未结束!它在执行完 findCombo(2, "((", 2, 0) 分支后,紧接着检查并执行了另一个合法分支:
if (usedOpen > usedClose && usedClose < n) {
findCombo(n, cur + ')', usedOpen, usedClose + 1);
}此时第二层状态为 cur = "(", usedOpen = 1, usedClose = 0 → 满足条件,于是启动新分支:
→ findCombo(2, "()", 1, 1)
此后路径为:
"()" → "()("(加 '(',因 usedOpen=1 < 2)→ "()()"(加 ')',因 1 > 1 不成立?等等——注意:此时 usedOpen=1, usedClose=1,usedOpen > usedClose 为假,但别急!
实际上,在 "()" 状态下(usedOpen=1, usedClose=1),下一步只能加 '('(因 usedOpen < 2),得到 "()(", 此时 usedOpen=2, usedClose=1 → 满足 usedOpen > usedClose,于是加 ')' 得 "()()" → 终止条件满足 → res.push("()()")。
由此可见,每一次 res.push() 只终结当前递归分支,而非整个搜索过程。函数通过自然的函数调用栈弹出机制,逐层回退,持续探索所有满足约束的未访问分支。
✅ 正确实现要点总结:
- 约束驱动剪枝:仅当 usedOpen < n 才加 '(';仅当 usedOpen > usedClose && usedClose < n 才加 ')',确保任意前缀中右括号数 ≤ 左括号数;
- 无显式回溯还原:因 cur 是字符串(值传递)、usedOpen/usedClose 是数字(不可变),每次递归都基于当前状态创建新参数,天然避免了手动 pop() 或重置;
- 全局收集结果:res 作为闭包变量或传参引用,持久保存所有合法解。
该算法时间复杂度为第 n 个卡特兰数 $ C_n = \frac{1}{n+1}\binom{2n}{n} $,空间复杂度为 $ O(n) $(递归栈深度)。掌握其回溯跳转逻辑,是理解 DFS 枚举类问题的基石。

















