目录

题目描述

22. 括号生成

image-20250510091050895

题意分析

给定括号对数 n,要求列出全部n'('n')' 组成的有效括号串,顺序任意但不能重复、不能遗漏。这是一道枚举题,答案是一个集合而不是一个数值,所以正确性要同时论证「生成的都合法」和「合法的都生成」。

先把「有效」这个词换成可以逐字符检查的条件。一个长度为 2n 的括号串有效,等价于同时满足两条:左括号总数与右括号总数各为 n;从左往右扫的过程中,任意前缀里右括号的数量都不超过左括号的数量。第二条是全题的支点——它不是对整个串的整体判断,而是对每个前缀都成立的局部判断,这意味着合法性可以在逐字符构造的过程中同步维护,不必等串写完再检验。

数据范围只到 n 不超过 8,答案个数是第 n 个卡特兰数(n = 8 时是 1430),这个规模明确告诉我们:本题不指望多项式复杂度,指数级枚举是被接受的。因此考点不在压低量级,而在于枚举得既不重复也不遗漏,并且尽量不去走那些注定失败的分支。

还有一个信号藏在「构造方式」里:串是从左到右一个字符一个字符长出来的,每一步只有两种选择。这种「位置固定、选项有限」的结构天然不会产生重复——两条不同的选择序列必然在某一位上字符不同,因此得到的字符串一定不同,答案里不需要额外去重。

需要留意的边界情形:n = 1 时唯一答案是 ["()"];题目保证 n 至少为 1,所以不必处理 n = 0;不要把「左括号用完」误当成「串已完成」,"((" 这样左括号刚好用满 n 个的前缀离合法还差整整 n 个右括号。

解法:回溯生成合法前缀

核心思路

回溯时只生成合法前缀。左括号未用完就可以添加 '(';只有右括号数量小于左括号数量时,才能添加 ')'。路径长度达到 2n 时得到一个答案。

解题步骤

  1. leftright 记录已使用的左右括号数量。
  2. left < n 时添加左括号,递归后撤销。
  3. right < left 时添加右括号,递归后撤销。
  4. 路径长度为 2n 时保存当前字符串。

代码实现

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        backtrack(n, 0, 0, new StringBuilder(), result);
        return result;
    }

    private void backtrack(int n, int left, int right,
                           StringBuilder path, List<String> result) {
        if (path.length() == 2 * n) {
            result.add(path.toString());
            return;
        }

        if (left < n) {
            path.append('(');
            backtrack(n, left + 1, right, path, result);
            path.deleteCharAt(path.length() - 1);
        }
        if (right < left) {
            path.append(')');
            backtrack(n, left, right + 1, path, result);
            path.deleteCharAt(path.length() - 1);
        }
    }
}
func generateParenthesis(n int) []string {
    result := make([]string, 0)
    path := make([]byte, 0, 2*n)

    var backtrack func(int, int)
    backtrack = func(left, right int) {
        if len(path) == 2*n {
            result = append(result, string(path))
            return
        }

        if left < n {
            path = append(path, '(')
            backtrack(left+1, right)
            path = path[:len(path)-1]
        }
        if right < left {
            path = append(path, ')')
            backtrack(left, right+1)
            path = path[:len(path)-1]
        }
    }

    backtrack(0, 0)
    return result
}

复杂度分析

  • 时间复杂度:$O(nC_n)=O(4^n/\sqrt{n})$,共生成 $C_n$ 个长度为 2n 的合法串。
  • 空间复杂度:$O(n)$,递归栈和路径长度均为 2n;不计返回结果。

关键点总结

  • right < left 保证任何前缀中右括号都不会多于左括号。
  • 两个分支必须分别判断,某些状态下需要同时尝试。
  • 使用同一可变路径,并在递归返回后撤销最后一次选择。

易错点总结

  • 把右括号条件写成 right < n,会生成非法前缀。
  • 左括号用完不等于字符串完成,终止条件应是长度达到 2n
  • 忘记回溯撤销,会污染后续分支。
  • Java 保存结果时要调用 path.toString(),不能保存可变对象本身。

相似题目

题目 难度 考察点
20. 有效的括号 简单 只判定单个串是否合法而不枚举,且有三种括号类型,需要用栈匹配而非两个计数器
LCR 085. 括号生成 中等 与本题同题换号,可原样复用两份代码,适合用来检验是否真正记住了两个分支条件
面试题 08.09. 括号 中等 同为生成全部合法括号串,但题面额外强调结果不得重复,适合用来论证这个构造过程为何天然无重复