力扣算法题——括号生成
题目描述
数字 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 的字符串。在构建的每一步,我们都有两种选择:
- 添加一个左括号 (
- 添加一个右括号 )
但我们不能随意添加,必须遵循以下两个约束条件(剪枝条件):
约束一:何时可以添加左括号 (?
只要我们还有剩余的左括号可用(即已使用的左括号数量小于 n),我们就可以添加一个左括号。
约束二:何时可以添加右括号 )?
只有当已使用的右括号数量严格小于已使用的左括号数量时,我们才能添加一个右括号。这个规则是保证括号有效性的关键。它确保了任何一个右括号都有一个在它之前的左括号与之匹配,避免了像 )( 或者 ()) 这种在某个前缀中右括号比左括号多的非法情况。
算法流程
用一个递归函数来模拟这个构建过程:
- 函数定义:我们需要一个递归函数,它需要知道当前的状态,包括:
- 当前已经构建好的字符串 currentString。
- 已经使用的左括号数量 leftUsed。
- 已经使用的右括号数量 rightUsed。
- 递归的终止条件(Base Case):
- 当 currentString 的长度达到 2n 时,说明我们已经构建完成了一个完整的、有效的括号组合。我们将这个字符串添加到最终的结果列表中,然后结束本次递归。
- 递归的探索过程(做出选择):
- 尝试添加左括号:检查是否满足约束一 (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(添加左括号)。
- currentString 变为 ((。
- leftUsed++ 执行,leftUsed 的值在当前函数中从 1 变成了 2。
- generate 递归调用,去探索 (( 开头的所有可能性。
- 假设 (( 的所有路径(比如 (()))都探索完了,递归返回。
- deleteCharAt 执行,currentString 恢复为 (。这部分是正确的!
- 现在,程序继续往下走,来到分支2(添加右括号)。
- 它会检查条件 if(rightUsed < leftUsed)。此时 rightUsed 是 0,但 leftUsed 是多少?它是在第3步中被修改后的值,是 2!
- 所以条件 0 < 2 为真,程序错误地认为可以添加右括号,然后继续探索 () 这条路径。

浙公网安备 33010602011771号