问题说明(含示例)

问题描述:给定一个整数 n(代表生成括号的对数),设计函数生成所有可能的、有效的括号组合。有效括号组合需满足 “左括号必须在对应右括号之前出现” 且 “左右括号总数均为 n”。

示例

输入输出解释
n = 3["((()))","(()())","(())()","()(())","()()()"]3 对括号的所有有效组合共 5 种,均满足 “左括号先于右括号” 且总数为 6 个字符
n = 1["()"]1 对括号仅 1 种有效组合,无其他可能
n = 2["(())","()()"]2 对括号的有效组合共 2 种,排除无效组合如)()((

解题关键

核心思路是回溯法结合有效括号规则,通过 “控制左右括号的生成顺序和数量” 直接过滤无效组合,避免先穷举再判断的低效逻辑,具体步骤如下:

  1. 初始化变量

    • result:存储所有有效括号组合(最终返回结果);
    • 回溯函数参数:current(当前构建的括号字符串)、left(已使用左括号数量)、right(已使用右括号数量)。
  2. 回溯核心逻辑

    • 终止条件:当 current 长度等于 2*nn 对括号总长度为 2n),将其加入 result
    • 生成左括号:若 left < n(左括号未超量),拼接左括号并递归;
    • 生成右括号:若 right < left(右括号不超前左括号),拼接右括号并递归。
  3. 启动回溯:从空字符串、left=0right=0 开始调用回溯函数,最终返回 result

核心逻辑 + 关键细节

一、核心逻辑:如何通过回溯生成有效组合?

回溯的本质是 “按规则探索所有合法路径”,具体流程可拆解为 3 步:

  1. 参数设计定方向:用 left 和 right 两个参数跟踪括号使用数量,而非仅靠字符串判断(如 “数当前字符串中左括号数量”),直接将 “有效规则” 转化为参数约束,减少计算开销。
  2. 递归条件控合法性
    • 左括号约束(left < n):确保左括号总数不超过 n(如 n=3 时,左括号最多用 3 个,避免生成 (((( 这类超量组合);
    • 右括号约束(right < left):确保右括号始终 “跟随” 左括号,避免生成 )(“右括号超前” 的无效组合(如 ()) 中,第二个 ) 对应的 right=2left=1,不满足 right < left,被禁止)。
  3. 终止条件收结果:当 len(current) == 2*n 时,说明已生成完整的 n 对括号(左括号 n 个、右括号 n 个),且因生成过程严格遵循规则,必然是有效组合,直接加入结果。

二、关键细节:避坑与优化点

  1. 为什么无需 pop() 回溯?Python 中字符串是不可变对象,每次拼接(如 current + '(')都会生成新字符串,而非修改原 current。例如:

    • 调用 backtrack(current + '(', left+1, right) 时,传递的是新字符串,原 current 仍为拼接前的状态;
    • 递归返回后,无需通过 pop() 撤销选择,原 current 可直接用于下一次右括号的拼接,简化代码。
  2. 为什么要先判断 “加左括号” 再判断 “加右括号”?顺序不影响正确性,但先加左括号更符合 “左括号优先” 的直觉,且能确保生成的组合按 “左括号尽可能多” 的顺序排列(如 ((())) 先于 (()())),与示例输出一致。若调换顺序,组合顺序会变为 “右括号尽可能早加”(如 ()()() 可能提前),但仍有效。

  3. 初始代码中 disct={'(':')'} 为什么要删除?该字典完全冗余:有效括号的左右括号是固定的(左为 (、右为 )),无需通过字典映射获取右括号,直接拼接 ) 即可。保留字典会增加内存占用和代码冗余,无任何实际作用。

  4. 如何避免生成重复组合?因回溯过程中,每个递归分支的 “左括号数量” 和 “右括号数量” 唯一(如 left=2、right=1 仅对应一种状态),且每次选择(加左 / 右括号)都是唯一路径,不会生成重复组合,无需额外去重(如用集合)。

对应代码

from typing import List

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        # 存储所有有效括号组合
        result = []
        
        # 回溯函数:current=当前括号字符串,left=已用左括号数,right=已用右括号数
        def backtrack(current: str, left: int, right: int) -> None:
            # 终止条件:字符串长度=2n(n对括号总长度),加入结果
            if len(current) == 2 * n:
                result.append(current)
                return
            
            # 条件1:左括号未超量(<n),可加左括号
            if left < n:
                backtrack(current + '(', left + 1, right)
            
            # 条件2:右括号未超前左括号(<left),可加右括号
            if right < left:
                backtrack(current + ')', left, right + 1)
        
        # 初始调用:空字符串开始,左、右括号数均为0
        backtrack("", 0, 0)
        return result

对应的基础知识

实现该算法需掌握以下 Python 基础概念与操作,是理解核心逻辑的前提:

1. 递归函数的定义与调用

  • 语法:函数内部调用自身,需包含 “终止条件”(避免无限递归)和 “递归体”(缩小问题规模);
  • 应用backtrack 函数通过递归探索 “加左括号” 和 “加右括号” 两个分支,直到生成完整的括号字符串;
  • 关键递归参数 left 和 right 是 “状态传递” 的核心,确保每次调用都能跟踪当前括号使用情况。

2. 字符串的不可变性与拼接(这里结合python的递归栈进行理解)

  • 不可变性:字符串创建后无法修改,如 current = "("current + "(" 会生成新字符串 "(( ",而非修改原 current
  • 拼接操作current + '(' 或 current + ')' 用于生成新的括号字符串,无需额外的 “添加 / 删除” 操作,简化回溯逻辑;
  • 优势:避免因修改原字符串导致的状态污染,无需 pop() 等撤销操作。

3. 条件判断语句

  • 语法if 条件: 执行代码,用于控制递归分支的合法性;
  • 应用
    • if left < n:过滤左括号超量的分支;
    • if right < left:过滤右括号超前的分支;
  • 作用:在生成过程中直接排除无效组合,比 “先穷举再判断有效性” 效率高得多。

4. 列表的 append() 方法

  • 语法列表.append(元素),将元素添加到列表末尾;
  • 应用result.append(current),将生成的有效括号字符串加入结果列表;
  • 注意:因 current 是字符串(不可变),加入列表后不会被后续操作修改,确保结果正确。

对应的进阶知识

该问题的背后涉及算法复杂度、组合数学(卡特兰数)与回溯优化的深层理解:

1. 时间复杂度:由卡特兰数决定

  • 卡特兰数:n 对有效括号的组合数是第 n 个卡特兰数,公式为 C(n) = (1/(n+1)) * C(2n, n)C(2n, n) 是 2n 个元素中选 n 个的组合数);
  • 量级估算:卡特兰数约等于 4^n / (n√n),因此时间复杂度为 O(4^n / √n)
  • 原因:每个有效组合的生成需 O(n) 时间(字符串拼接,长度为 2n),总时间与组合数成正比,即 O(4^n / √n)

2. 空间复杂度:O(n)

  • 递归调用栈:递归深度最大为 2n(生成最长字符串 ((...)) 时,需 2n 层递归),但因有效规则限制(右括号不超前左括号),实际栈深不超过 2n
  • 字符串存储:每个递归分支的 current 是独立字符串,总空间与递归栈深度成正比,即 O(n)(非结果列表的空间,结果列表属于输出必要空间)。

3. 与 “全排列 / 子集” 的回溯差异

问题类型回溯核心差异撤销操作约束条件
生成有效括号按 “规则生成”(左 < n、右 < 左)无需(字符串不可变)强约束(避免无效组合)
全排列按 “元素不重复选”需(列表 pop()弱约束(仅元素不重复)
子集按 “索引不回头选”需(列表 pop()弱约束(仅索引控制)
  • 核心区别:生成有效括号的回溯是 “规则驱动”,通过强约束直接过滤无效路径;而全排列 / 子集是 “范围驱动”,需通过索引或标记控制范围。

4. 优化思路:避免冗余计算

  • 无需先穷举再判断:若不按规则生成,而是先穷举所有 2^(2n) 种括号组合(如 ((()) 等),再判断有效性,时间复杂度会达到 O(2^(2n) * n)(判断每个组合需 O(n) 时间),远高于 O(4^n / √n)
  • 参数传递优化:用 left 和 right 直接跟踪数量,而非每次通过 current.count('(') 计算左括号数量(后者每次计算需 O(n) 时间,会增加时间复杂度)。

编程思维与启示

1. “约束先行”:用规则代替事后过滤

  • 思维核心:不盲目探索所有可能,而是在生成过程中用 “约束条件”(左 < n、右 < 左)直接排除无效路径,效率远高于 “先穷举再筛选”;
  • 应用场景:适用于 “有效解有明确规则” 的问题(如合法 IP 地址生成、括号匹配类问题),避免无效计算。

2. 利用数据类型特性简化代码

  • 思维核心:根据数据类型的特性(如字符串不可变、列表可变)设计回溯逻辑,减少 “撤销操作” 的代码冗余;
  • 示例:字符串不可变让我们无需 pop(),列表可变让全排列需 pop(),选择合适的数据类型可简化代码。

3. 参数设计:跟踪 “关键状态”

  • 思维核心:回溯函数的参数需包含 “当前进度”(current)和 “约束条件所需的状态”(leftright),避免在函数内部重复计算状态(如每次数左括号数量);
  • 启示:设计参数时,反问自己 “需要哪些信息才能判断下一步是否合法?”,将这些信息作为参数传递,提升效率。

4. 边界处理:终止条件的精准性

  • 思维核心:终止条件需与 “问题目标” 直接挂钩(如 len(current) == 2n 对应 “生成 n 对括号”),避免模糊的终止条件(如 “左括号 == n 且右括号 == n”,虽等效,但 len(current) == 2n 更直观);
  • 启示:终止条件应 “可直接验证”,减少逻辑判断的复杂度。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐