LeetCode 22. 括号生成:DFS回溯解法详解

0 阅读6分钟

在LeetCode的字符串类题目中,「括号生成」绝对是回溯算法的经典入门题——它不仅考察对括号有效性的判断,更考验如何通过递归回溯,高效枚举所有合法组合,避免无效枚举带来的冗余计算。今天就来拆解这道题,从思路分析到代码实现,再到细节优化,带你彻底搞懂这道高频面试题。

一、题目回顾:明确需求与核心难点

题目描述:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

输入:n = 3 → 输出:["((()))","(()())","(())()","()(())","()()()"]

输入:n = 1 → 输出:["()"]

核心难点拆解:

  1. 有效性约束:括号必须成对出现,且左括号必须在右括号之前(比如 "())(" 是无效的);

  2. 枚举完整性:要列出所有合法组合,不能遗漏,也不能出现重复;

  3. 效率优化:避免生成无效组合(比如左括号数量超过n、右括号数量超过左括号),减少不必要的递归。

二、思路分析:为什么用DFS回溯?

括号生成的过程,本质上是「逐步构建字符串」的过程——每一步都有两个选择:加左括号 ( 或加右括号 )。但这两个选择并非无限制,必须满足两个核心条件,才能保证生成的括号有效:

  • 左括号数量不能超过n(因为总共只有n对括号,左括号最多n个);

  • 右括号数量不能超过当前左括号数量(否则会出现 "())" 这种无效情况)。

基于这两个条件,我们可以用「深度优先搜索(DFS)」+「回溯」的思路来解题:

  1. 定义递归函数,参数记录当前左括号数量、括号平衡度(左括号数 - 右括号数)、当前构建的字符串;

  2. 递归终止条件:当左括号数量等于n,且平衡度为0(说明左、右括号数量相等,且都达到n个),将当前字符串加入结果集;

  3. 递归分支:

    • 如果左括号数量小于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;
};

逐行解析核心细节

  1. 结果数组 res:用于存储所有合法的括号组合,递归终止时将符合条件的字符串加入。

  2. 递归函数 dfs 的三个参数:

    • openCount:记录当前已经使用了多少个左括号,用于限制左括号总数不超过n;

    • balance:平衡度,核心作用是判断是否能添加右括号——balance = 左括号数 - 右括号数,只有balance > 0时,添加右括号才不会导致无效;

    • currentStr:当前正在构建的字符串,每递归一次,就拼接一个括号。

  3. 终止条件:当 openCount === n(左括号用完)且 balance === 0(右括号数等于左括号数),说明当前字符串是合法的,加入res。

  4. 两个递归分支:

    • 添加左括号:只有 openCount< n 时才能执行,此时左括号数+1,平衡度+1(因为多了一个左括号);

    • 添加右括号:只有 balance > 0 时才能执行,此时平衡度-1(因为多了一个右括号),openCount 不变(右括号不影响左括号数量)。

  5. 初始调用:从空字符串开始,左括号数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、右括号不超过左括号),就能高效生成所有合法组合。

这道题的价值不仅在于解题本身,更在于掌握「回溯算法的剪枝思想」——在枚举过程中,提前过滤无效路径,减少冗余计算,这是很多回溯题(如子集、排列)的核心解题思路。