力扣算法题——括号生成

题目描述

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

示例 1:

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

示例 2:

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

解题标签

回溯

知识补充

一、回溯和递归的区别?

答:简单来说,递归是一种“自己调用自己”的代码实现手段;回溯是一种“系统地试错并撤销选择”的算法思想,通常用递归来实现。

• 递归 = 语法级概念:函数体内再次调用自身。
• 回溯 = 算法级概念:在搜索空间里逐层做选择 → 遇到死路 → 撤销选择 → 换路继续。

二、StringBuilder的一些知识

相比于String,StringBuilder在做频繁拼接,删除,插入时不会产生大量的中间对象,因此效率更高。从“增,删,改,查”四个方面我们来介绍StringBuilder的方法

方法 说明
append(x) 把任意类型(基本类型、char[]、String、Object…)转成字符串追加到末尾
insert(offset, x) 在指定下标之前插入任意类型数据。offset 必须在 [0, length] 之间。
方法 说明
delete(start, end) 删除 [start, end) 区间的字符。
deleteCharAt(index) 删除单个字符,等价于 delete(index, index+1)
方法 说明
replace(start, end, str) [start, end) 区间内容替换为给定字符串 str
setCharAt(index, ch) 把指定下标的字符替换成新的字符。
reverse() 原地反转字符序列,并返回当前 StringBuilder 自身(链式调用)。
方法 说明
charAt(index) 读取指定下标的字符。
length() 当前字符个数。
substring(start, end) 截取 [start, end) 区间的字符串(返回新的 String)。
indexOf(str) / indexOf(str, fromIndex) 正向查找子串首次出现位置,找不到返回 -1
lastIndexOf(str) / lastIndexOf(str, fromIndex) 反向查找。
  • 其他方法
方法 说明
capacity() 查看当前内部字符数组容量
ensureCapacity(min) 提前扩容,避免后续追加触发多次扩容。
trimToSize() 把内部数组缩到恰好容纳现有字符,节省内存。
toString() 把可变序列变成不可变String,用于最终输出。

解题思路

使用回溯法进行“约束性构建”

我们的目标是构建一个长度为 2n 的字符串。在构建的每一步,我们都有两种选择:

  1. 添加一个左括号 (
  2. 添加一个右括号 )

但我们不能随意添加,必须遵循以下两个约束条件(剪枝条件)

约束一:何时可以添加左括号 (?

只要我们还有剩余的左括号可用(即已使用的左括号数量小于 n),我们就可以添加一个左括号。

约束二:何时可以添加右括号 )?

只有当已使用的右括号数量严格小于已使用的左括号数量时,我们才能添加一个右括号。这个规则是保证括号有效性的关键。它确保了任何一个右括号都有一个在它之前的左括号与之匹配,避免了像 )( 或者 ()) 这种在某个前缀中右括号比左括号多的非法情况。

算法流程

用一个递归函数来模拟这个构建过程:

  1. 函数定义:我们需要一个递归函数,它需要知道当前的状态,包括:
    • 当前已经构建好的字符串 currentString。
    • 已经使用的左括号数量 leftUsed。
    • 已经使用的右括号数量 rightUsed。
  2. 递归的终止条件(Base Case)
    • 当 currentString 的长度达到 2n 时,说明我们已经构建完成了一个完整的、有效的括号组合。我们将这个字符串添加到最终的结果列表中,然后结束本次递归。
  3. 递归的探索过程(做出选择)
    • 尝试添加左括号:检查是否满足约束一 (leftUsed < n)。如果满足,就拼接一个 ( 到 currentString,并带着更新后的状态 (leftUsed + 1)继续递归下去。
    • 尝试添加右括号:检查是否满足约束二 (rightUsed < leftUsed)。如果满足,就拼接一个 ) 到 currentString,并带着更新后的状态 (rightUsed + 1)继续递归下去。

代码实现

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList<>();
        generate(list, new StringBuilder(""), 0, 0, n);
        return list;
    }
    public void generate(List<String> result, StringBuilder currentString, int leftUsed, int rightUsed, int n){
        if(currentString.length() == 2*n){
            result.add(currentString.toString());
            return;
        }
        /** 探索过程开始 */
        // 1.尝试添加左括号
        if(leftUsed < n){
            currentString.append('(');
            generate(result, currentString, leftUsed+1, rightUsed, n);
            currentString.deleteCharAt(currentString.length()-1);
        }
        // 2.尝试添加右括号
        if(rightUsed < leftUsed){
            currentString.append(')');
            generate(result, currentString, leftUsed, rightUsed+1, n);
            currentString.deleteCharAt(currentString.length()-1);
        }
    }
}

难点分析

1.回溯的辅助函数通常不应该有返回值,在我们这道题中,generate就是一个回溯辅助函数,它的返回类型时void。这是因为回溯函数的作用不是返回结果,而是“当找到一个符合条件的结果时,将它添加到我们从外部传入的结果集中”,在这道题中,“外部传入的结果集”指的是我们传入的list

2.在下面的代码片段中,一定不能写成leftUsed+1的逻辑传入到generate中,一定不能写成

// 错误写法
leftUsed++;
generate(result, currentString, leftUsed, rightUsed, n);
// 正确写法
if(leftUsed < n){
    currentString.append('(');
    generate(result, currentString, leftUsed+1, rightUsed, n);
    currentString.deleteCharAt(currentString.length()-1);
}
if(rightUsed < leftUsed){
    currentString.append(')');
    generate(result, currentString, leftUsed, rightUsed+1, n);
    currentString.deleteCharAt(currentString.length()-1);
}

原因分析:如果我们把leftUsed++写在generate外面而不是写在generate的形参中就会出现“状态污染”

想象一下当 n=2,currentString 为 (,leftUsed 为 1,rightUsed 为 0 时的情况:

  1. 程序进入分支1(添加左括号)。
  2. currentString 变为 ((。
  3. leftUsed++ 执行,leftUsed 的值在当前函数中从 1 变成了 2
  4. generate 递归调用,去探索 (( 开头的所有可能性。
  5. 假设 (( 的所有路径(比如 (()))都探索完了,递归返回。
  6. deleteCharAt 执行,currentString 恢复为 (。这部分是正确的!
  7. 现在,程序继续往下走,来到分支2(添加右括号)。
  8. 它会检查条件 if(rightUsed < leftUsed)。此时 rightUsed 是 0,但 leftUsed 是多少?它是在第3步中被修改后的值,是 2
  9. 所以条件 0 < 2 为真,程序错误地认为可以添加右括号,然后继续探索 () 这条路径。
posted @ 2025-07-25 22:23  平生三伏时  阅读(221)  评论(0)    收藏  举报