(leetcode)力扣100 59括号生成(回溯||按括号序列的长度递归)

题目

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

数据范围

1 <= n <= 8

测试用例

示例1

bash 复制代码
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例2

bash 复制代码
输入:n = 1
输出:["()"]

题解1(博主题解,时间复杂度O(4nn)O(\frac{4^n}{\sqrt{n}})O(n 4n),空间复杂度ON)

java 复制代码
class Solution {
    // 全局变量,用于在递归过程中跟踪左括号 '(' 的数量
    int leftNum = 0;
    // 全局变量,用于在递归过程中跟踪右括号 ')' 的数量
    int rightNum = 0;
    // 用于存储所有生成的有效括号组合结果
    List<String> res;

    public List<String> generateParenthesis(int n) {
        res = new ArrayList<>();
        // 从 0 个字符开始递归,初始字符串为空
        dfs(n, 0, new StringBuilder());
        return res;
    }

    /**
     * 深度优先搜索函数
     * @param n    题目要求的括号对数
     * @param curr 当前构建的字符串长度 (也是递归深度)
     * @param sb   当前路径构建的字符串
     */
    public void dfs(int n, int curr, StringBuilder sb) {
        // [剪枝条件]
        // 如果在任何时候,右括号数量超过了左括号,这一定是非法的(例如 "())"),直接停止该路径的搜索
        if (leftNum < rightNum) {
            return;
        }

        // [递归终止条件 / Base Case]
        // 当构建的字符串长度达到 2*n 时(即填满了所有位置)
        if (curr == 2 * n) {
            // 只有当左括号数量等于右括号数量时,才是一个有效的解
            if (leftNum == rightNum) {
                res.add(sb.toString());
            }
            return; // 结束当前递归分支
        }

        // [递归逻辑]
        // 循环两次,i=0 尝试添加左括号,i=1 尝试添加右括号
        // 注意:这种写法虽然逻辑正确,但比通常的写法稍微低效,因为它会尝试添加超过 n 个的左括号,直到最后才被淘汰
        for (int i = 0; i < 2; i++) {
            if (i == 0) { 
                // --- 尝试添加左括号 ---
                sb.append("(");         // 做出选择:添加 '('
                leftNum++;              // 更新状态:左括号计数 +1
                
                dfs(n, curr + 1, sb);   // 进入下一层递归
                
                // --- 回溯 (Backtracking) ---
                // 撤销选择:当递归返回后,需要将状态恢复到进入递归之前,以便进行下一次循环尝试
                leftNum--;              // 恢复状态:左括号计数 -1
                sb.delete(curr, curr + 1); // 撤销选择:删除刚才添加的 '('
            } 
            else {
                // --- 尝试添加右括号 ---
                sb.append(")");         // 做出选择:添加 ')'
                rightNum++;             // 更新状态:右括号计数 +1
                
                dfs(n, curr + 1, sb);   // 进入下一层递归
                
                // --- 回溯 (Backtracking) ---
                rightNum--;             // 恢复状态:右括号计数 -1
                sb.delete(curr, curr + 1); // 撤销选择:删除刚才添加的 ')'
            }
        }
    }
}

题解2(官解,时空同上)

java 复制代码
class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> ans = new ArrayList<String>();
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
        if (cur.length() == max * 2) {
            ans.add(cur.toString());
            return;
        }
        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open + 1, close, max);
            cur.deleteCharAt(cur.length() - 1);
        }
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close + 1, max);
            cur.deleteCharAt(cur.length() - 1);
        }
    }
}

题解3(官解2 按括号序列的长度递归,时间同上,空间同时间)

java 复制代码
class Solution {
    ArrayList[] cache = new ArrayList[100];

    public List<String> generate(int n) {
        if (cache[n] != null) {
            return cache[n];
        }
        ArrayList<String> ans = new ArrayList<String>();
        if (n == 0) {
            ans.add("");
        } else {
            for (int c = 0; c < n; ++c) {
                for (String left: generate(c)) {
                    for (String right: generate(n - 1 - c)) {
                        ans.add("(" + left + ")" + right);
                    }
                }
            }
        }
        cache[n] = ans;
        return ans;
    }

    public List<String> generateParenthesis(int n) {
        return generate(n);
    }
}

思路

这道题暴力搜索不在我们的考虑范围之内,我们直接选择回溯,回溯方法很简单,java存在stringbuilder与stringbuffer字符串操作也很轻松。是一个较为轻松的题,博主的回溯法与官解的回溯法大体思路一样,但是官解的细节处理要好一些,博主的方法会多进一次递归,虽然影响不大,但总归是没有官解的好。

这道题还有按照括号序列的长度递归这个方法,采用了分治的思路,由于性能不错,大家也可以学习学习,由于博主分治还不是很熟练,就不讲解这个方法的思路了,直接贴上ai讲解!!

这份官方题解的代码采用了一种完全不同的思路:分治算法(Divide and Conquer) 结合 记忆化搜索(Memoization / Dynamic Programming)。这种方法的数学核心是:任何一个合法的括号序列,都可以被拆分为 ( A ) B 的形式。

  1. 核心逻辑:( left ) right不同于之前的"回溯法"是一个字符一个字符地拼凑,这份代码是成块成块地组装括号。对于任何一个长度为 nnn 的合法括号序列,它一定以 ( 开头。这个起始的 ( 一定有一个与之匹配的 )。我们可以把这个序列看作:( 内部 ) 外部(\ \text{内部} \ ) \ \text{外部}( 内部 ) 外部代码中的这一行是灵魂:
java 复制代码
ans.add("(" + left + ")" + right);

left (内部):是在那一对括号 里面 的合法序列。right (外部):是在那一对括号 后面 的合法序列。数量关系:总共需要 nnn 对括号。最外层已经用掉了 1 对 ( ... )。假设我们在 内部 放了 ccc 对括号。那么 外部 剩下的就必须是 n−1−cn - 1 - cn−1−c 对括号。

  1. 代码逐行详解A. 缓存(Memoization)
java 复制代码
ArrayList[] cache = new ArrayList[100];

public List<String> generate(int n) {
    if (cache[n] != null) {
        return cache[n];
    }
    // ... 计算过程 ...
    cache[n] = ans; // 计算完后存入缓存
    return ans;
}

作用:这是一个"备忘录"。因为计算 generate(3) 时可能会用到 generate(2),计算 generate(4) 时也会用到 generate(2)。

避免重复计算:如果不存下来,程序会反复计算相同的 nnn,效率会非常低。有了 cache,每个 nnn 只会被计算一次。

B. 递归与分治

java 复制代码
if (n == 0) {
    ans.add("");
} else {
    // c 代表括号对内部的括号数量
    for (int c = 0; c < n; ++c) {
        // 遍历内部所有可能的组合 (left)
        for (String left: generate(c)) {
            // 遍历外部所有可能的组合 (right)
            // 外部的数量 = 总数 n - 1 (外层那对) - c (内部占用的)
            for (String right: generate(n - 1 - c)) {
                // 组合起来:(内部)外部
                ans.add("(" + left + ")" + right);
            }
        }
    }
}

这里的双重循环 for (String left...) 和 for (String right...) 是一个笛卡尔积。它把所有可能的"内部结构"和"外部结构"拼在一起。

  1. 举例演示 (n=3n=3n=3)

我们需要生成 3 对括号的所有组合。公式:( left ) right,其中 left 有 ccc 对,right 有 3−1−c3-1-c3−1−c 对。

情况 1:c=0c=0c=0 (内部 0 对,外部 2 对)

left: "" (0对)

right: generate(2) 的结果 -> ()(), (())

组合:

( + "" + ) + ()() = ()()()

( + "" + ) + (()) = ()(())

情况 2:c=1c=1c=1 (内部 1 对,外部 1 对)

left: generate(1) -> ()

right: generate(1) -> ()

组合:

( + () + ) + () = (())()

情况 3:c=2c=2c=2 (内部 2 对,外部 0 对)

left: generate(2) -> ()(), (())

right: "" (0对)

组合:

( + ()() + ) + "" = (()())

( + (()) + ) + "" = ((()))

结果:()()(), ()(()), (())(), (()()), ((())) (共5种,完全正确)

  1. 复杂度分析时间复杂度:依然是卡特兰数级别 O(4nn)O(\frac{4^n}{\sqrt{n}})O(n 4n)。因为无论怎么算,结果的总数没变。空间复杂度:由于使用了 cache 数组存储所有中间结果,以及大量的字符串拼接对象,空间消耗通常比回溯法要大一些,但逻辑结构更清晰(特别是对于理解动态规划而言)。总结上一份代码 (回溯法) 是在"走路",一步步试探,撞墙回头。这份代码 (分治/动态规划) 是在"搭积木"。它把大问题拆成 (左边) 和 右边 两个小问题,分别解决后再拼起来。这份代码的优势在于逻辑非常符合卡特兰数的递归定义公式:Cn=∑i=0n−1CiCn−1−iC_n = \sum_{i=0}^{n-1} C_i C_{n-1-i}Cn=i=0∑n−1CiCn−1−i如果你需要解决类似的数学构造问题,这种分治思维非常重要。
相关推荐
共享家95272 小时前
双指针算法(一)
数据结构·算法·leetcode
十八岁讨厌编程2 小时前
【算法训练营 · 二刷总结篇】回溯算法、动态规划部分
算法·动态规划
近津薪荼2 小时前
优选算法——滑动窗口2(数组模拟哈希表)
c++·学习·算法
金枪不摆鳍2 小时前
算法基础-哈希表
算法·哈希算法
渐暖°2 小时前
【leetcode算法从入门到精通】9. 回文数
算法·leetcode·职场和发展
星火开发设计2 小时前
using 关键字:命名空间的使用与注意事项
开发语言·c++·学习·算法·编程·知识
ZPC82102 小时前
机器人手眼标定
人工智能·python·数码相机·算法·机器人
知我心·2 小时前
Java实现常见算法
算法
HalvmånEver2 小时前
Linux:线程创建与终止下(线程六)
linux·运维·算法