Problem: 22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
整体思路
这段代码旨在解决一个经典的组合生成问题:括号生成 (Generate Parentheses)。问题要求给定一个整数 n
,生成所有由 n
对括号组成的、格式正确的括号组合。
该算法采用了 深度优先搜索 (DFS) 结合 剪枝 (Pruning) 的策略来构建所有合法的括号字符串。它通过维护左右括号的数量来确保在构建过程中的每一步都满足括号合法性的基本规则。
-
算法核心:递归与状态
- 算法的主体是一个
dfs
递归函数。这个函数负责在字符数组path
中逐步构建括号字符串。 - 核心状态:
dfs
函数通过三个关键参数来追踪构建过程:left
: 已使用的左括号(
的数量。right
: 已使用的右括号)
的数量。n
: 需要生成的括号对数,是一个固定目标。
path
数组用于存储当前正在构建的字符串,其总长度为2n
。left + right
恰好是当前要填充的path
数组的索引。
- 算法的主体是一个
-
递归的终止条件
if (right == n)
:当已使用的右括号数量达到n
时,意味着已使用的左括号数量也必然达到了n
(因为right
不可能超过left
),此时一个长度为2n
的完整且合法的括号字符串已经构建完成。- 将
path
字符数组转换为字符串new String(path)
,并将其加入最终结果ans
列表中,然后返回。
-
探索与剪枝(核心选择逻辑)
-
在
dfs
的每一步,我们都有两种可能的操作:添加一个左括号或添加一个右括号。但这些操作必须在满足特定条件(即剪枝规则)时才能进行,以保证字符串的合法性。
a. 添加左括号(
:- 条件:
if (left < n)
。只要已使用的左括号数量还没有达到n
,我们就可以安全地添加一个新的左括号。 - 操作:将
(
放入path
的当前位置,然后进行递归调用dfs(left + 1, right, ...)
,更新状态。
b. 添加右括号
)
:- 条件:
if (right < left)
。这是一个至关重要的剪枝规则。为了保证括号的合法性,任何时候,已使用的右括号数量都不能超过左括号的数量。只有当right < left
时,添加一个右括号才不会破坏这个规则。 - 操作:将
)
放入path
的当前位置,然后进行递归调用dfs(left, right + 1, ...)
,更新状态。
- 条件:
-
-
隐式回溯
- 这个实现中没有显式的“撤销选择”操作(如
path.removeLast()
)。这是因为它使用的是一个固定大小的字符数组path
。当一个递归分支返回后,上一个调用栈帧会继续执行,它可能会在path
的同一个位置上覆盖新的字符(例如,先尝试放(
,返回后再尝试放)
)。这种通过覆盖实现状态恢复的方式,是一种隐式的回溯。
- 这个实现中没有显式的“撤销选择”操作(如
完整代码
class Solution {
/**
* 生成所有 n 对有效括号的组合。
* @param n 括号的对数
* @return 所有有效括号组合的列表
*/
public List<String> generateParenthesis(int n) {
// ans: 存储所有符合条件的最终组合
List<String> ans = new ArrayList<>();
// path: 一个字符数组,用于构建括号字符串,总长度为 2n
char[] path = new char[n * 2];
// 开始深度优先搜索,初始时左右括号都为0
dfs(0, 0, n, ans, path);
return ans;
}
/**
* 深度优先搜索(DFS)辅助函数
* @param left 已使用的左括号 '(' 的数量
* @param right 已使用的右括号 ')' 的数量
* @param n 目标括号对数
* @param ans 结果列表
* @param path 当前正在构建的字符数组路径
*/
private void dfs(int left, int right, int n, List<String> ans, char[] path) {
// 递归终止条件:当已使用的右括号数量达到 n 时,
// 说明一个完整的、长度为 2n 的合法括号串已经构建完成。
if (right == n) {
// 将字符数组路径转换为字符串,并加入结果集
ans.add(new String(path));
return;
}
// --- 核心选择与剪枝逻辑 ---
// 选择 1: 放置一个左括号 '('
// 条件(剪枝):只要已使用的左括号数量还未达到 n,就可以放置。
if (left < n) {
// 将 '(' 放置在当前路径的末尾 (索引为 left + right)
path[left + right] = '(';
// 递归进入下一层,更新 left 的数量
dfs(left + 1, right, n, ans, path);
}
// 选择 2: 放置一个右括号 ')'
// 条件(剪枝):为了保证括号合法性,任何时候右括号的数量都不能超过左括号。
if (right < left) {
// 将 ')' 放置在当前路径的末尾
path[left + right] = ')';
// 递归进入下一层,更新 right 的数量
dfs(left, right + 1, n, ans, path);
}
}
}
时空复杂度
时间复杂度:O(C_n) 或 O(4^n / n^(3/2))
- 这个问题的解的数量是卡特兰数 (Catalan number),第
n
个卡特兰数C_n
约等于4^n / (n * sqrt(πn))
。 - DFS 算法会访问一个搜索树,该树的叶子节点对应着每个有效的括号组合。
- 在每个叶子节点,我们需要 O(n) 的时间来创建一个新的字符串
new String(path)
。 - 搜索树的节点总数与卡特兰数成正比。总的来说,算法的时间复杂度与解的数量和每个解的长度有关。
- 因此,时间复杂度可以表示为
O(n * C_n)
,或者更紧凑地写为 O(4^n / n^(3/2))。这是一个指数级复杂度。
空间复杂度:O(n)
- 递归栈深度:递归的最大深度发生在构建一条形如
((...))
的路径时,此时会连续调用n
次来放置左括号,然后再调用n
次来放置右括号。因此,递归栈的最大深度是2n
。所以,这部分空间复杂度是 O(n)。 path
数组:path
字符数组的长度是2n
,占用了 O(n) 的空间。- 结果列表
ans
:存储最终结果的空间不计入算法的额外辅助空间复杂度。
综合分析:
算法所需的额外空间由递归栈和 path
数组决定,两者都是 O(n) 级别。因此,空间复杂度为 O(n)。
参考灵神