22. 括号生成

0 阅读2分钟

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= n <= 8

1. 生活案例:搭建平衡木桥

想象你在玩一个搭建平衡木桥的游戏:

  • 材料:你有 nn 块左木板 (nn 块右木板 )

  • 规则

    1. 左木板先行:你手里必须先有左木板,才能往右边搭。
    2. 保持平衡:在任何时候,你已经搭上去的右木板数量,都不能超过左木板的数量(否则桥会塌,即括号无效)。
    3. 用完为止:当 2n2n 块木板全部搭完,且符合上述规则,你就建成了一座合法的桥。

2. 代码实现与详细注释

这是你图片中的代码,我为你添加了详细的“筑墙规则”注释:

JavaScript

/**
 * @param {number} n - 括号的对数
 * @return {string[]} - 所有有效的组合
 */
var generateParenthesis = function(n) {
    let res = []; // 【仓库】:存放所有盖好的合格建筑

    /**
     * @param {string} str - 当前已经搭好的部分
     * @param {number} left - 已经用了多少个左括号
     * @param {number} right - 已经用了多少个右括号
     */
    function backtrack(str, left, right) {
        // 【终点】:如果长度达到了 2n,说明木板全用完了
        if (str.length === 2 * n) {
            res.push(str); // 竣工,存入仓库
            return;
        }

        // 【规则1】:只要左括号还没用完(小于 n),就可以继续放左括号
        if (left < n) {
            backtrack(str + '(', left + 1, right);
        }

        // 【规则2】:只有当“已经放下的右括号”比“左括号”少时,才能放右括号
        // 这保证了每一个 ')' 之前一定有一个 '(' 对应,不会出现 ")(" 这种无效情况
        if (right < left) {
            backtrack(str + ')', left, right + 1);
        }
    }

    // 从空地、0个左括号、0个右括号开始搭
    backtrack('', 0, 0);
    return res;
};

3. 核心原理解析

为什么这个逻辑能过滤掉无效括号?

回溯算法在这里其实是在走一棵“决策树”。传统的暴力法会生成 22n2^{2n} 种组合然后再判断是否有效,而这个算法在生长的过程中就剪掉了错枝

  • 比如 n=2n=2,当路径变成 ()) 时,由于 right(2) > left(1),这个分支在代码逻辑里根本不会被触发。
  • 这大大减少了计算量,确保生出来的每一个成品都是合格的。

运行轨迹追踪(以 n=2n=2 为例)

  1. 开始:"" (0,0)
  2. 放左:"( (1,0)
  3. 放左:"((" (2,0) \rightarrow 此时只能放右:"(()" (2,1) \rightarrow 再放右:"(())" (2,2) 【完成1】
  4. 回到第2步,改放右:"()" (1,1) \rightarrow 此时只能放左:"()(" (2,1) \rightarrow 再放右:"()()" (2,2) 【完成2】

复杂度分析

  • 时间复杂度:这是一个著名的**卡特兰数(Catalan number)**问题。复杂度约为 O(4nn)O(\frac{4^n}{\sqrt{n}})
  • 空间复杂度O(n)O(n),主要是递归搜索树的深度。