问题描述
括号生成是一道经典的回溯算法题目。给定 n 对括号,编写一个函数来生成所有由 n 对括号组成的合法组合。合法括号序列要求每个左括号都有对应的右括号,且括号正确嵌套。
示例(n = 3):
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
算法思路分析
方法一:回溯法(深度优先搜索)
在生成括号的过程中,维护两个计数器:left(已用左括号数)和 right(已用右括号数)。
- 只要
left < n,就可以添加左括号 - 只要
right < left,就可以添加右括号(保证合法性) - 当
left == right == n时,得到一个合法序列
方法二:卡特兰数公式
合法括号序列的数量等于第 n 个卡特兰数:
C(n) = (2n)! / ((n+1)! * n!)
卡特兰数在组合数学中广泛出现,包括二叉树形态数、出栈序列数等。
Java 完整实现
import java.util.ArrayList;
import java.util.List;
public class GenerateParentheses {
/**
* 方法一:回溯法生成所有合法括号序列
* 时间复杂度:O(4^n / √n),空间复杂度:O(n)
*/
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
StringBuilder current = new StringBuilder();
backtrack(result, current, 0, 0, n);
return result;
}
/**
* 回溯核心逻辑
* @param result 结果集合
* @param current 当前构建的字符串
* @param left 已使用的左括号数
* @param right 已使用的右括号数
* @param n 括号对数
*/
private void backtrack(List<String> result, StringBuilder current,
int left, int right, int n) {
// 终止条件:所有括号都已用完
if (left == n && right == n) {
result.add(current.toString());
return;
}
// 选择1:添加左括号(只要还有剩余的左括号)
if (left < n) {
current.append('(');
backtrack(result, current, left + 1, right, n);
current.deleteCharAt(current.length() - 1); // 回溯
}
// 选择2:添加右括号(必须保证右括号少于左括号)
if (right < left) {
current.append(')');
backtrack(result, current, left, right + 1, n);
current.deleteCharAt(current.length() - 1); // 回溯
}
}
/**
* 方法二:递归构造(按括号嵌套层数分类)
* 时间复杂度:O(4^n / √n)
*/
public List<String> generateParenthesisDivide(int n) {
List<String> result = new ArrayList<>();
if (n == 0) {
result.add("");
return result;
}
// 外层括号内包含 p 对括号,外层右侧包含 (n-1-p) 对
for (int p = 0; p < n; p++) {
List<String> leftList = generateParenthesisDivide(p);
List<String> rightList = generateParenthesisDivide(n - 1 - p);
for (String left : leftList) {
for (String right : rightList) {
result.add("(" + left + ")" + right);
}
}
}
return result;
}
/**
* 方法三:动态规划
* dp[i] 存储 i 对括号的所有合法序列
*/
public List<String> generateParenthesisDP(int n) {
if (n == 0) {
List<String> base = new ArrayList<>();
base.add("");
return base;
}
@SuppressWarnings("unchecked")
List<String>[] dp = new ArrayList[n + 1];
dp[0] = new ArrayList<>();
dp[0].add("");
for (int i = 1; i <= n; i++) {
dp[i] = new ArrayList<>();
for (int j = 0; j < i; j++) {
for (String left : dp[j]) {
for (String right : dp[i - 1 - j]) {
dp[i].add("(" + left + ")" + right);
}
}
}
}
return dp[n];
}
/**
* 计算合法括号序列的数量(卡特兰数)
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public long catalanNumber(int n) {
long result = 1;
// 利用递推公式:C(n) = C(n-1) * 2*(2n-1) / (n+1)
for (int i = 0; i < n; i++) {
result = result * 2 * (2 * i + 1) / (i + 2);
}
return result;
}
/**
* 大数版本的卡特兰数(使用 long,可计算到 n=33)
*/
public long catalanDirect(int n) {
// C(2n, n) / (n + 1)
long combination = 1;
for (int i = 0; i < n; i++) {
combination = combination * (2L * n - i) / (i + 1);
}
return combination / (n + 1);
}
/**
* 验证括号序列是否合法
*/
public boolean isValid(String s) {
int balance = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
balance++;
} else {
balance--;
}
// 中间过程不能出现负值
if (balance < 0) return false;
}
return balance == 0;
}
public static void main(String[] args) {
GenerateParentheses solution = new GenerateParentheses();
int n = 3;
System.out.println("=== 括号生成问题 (n=" + n + ") ===");
List<String> result1 = solution.generateParenthesis(n);
System.out.println("回溯法结果 (" + result1.size() + " 个):");
for (String s : result1) {
System.out.println(" " + s + " -> 合法? " + solution.isValid(s));
}
List<String> result2 = solution.generateParenthesisDivide(n);
System.out.println("\n分治法结果 (" + result2.size() + " 个):");
for (String s : result2) {
System.out.println(" " + s);
}
List<String> result3 = solution.generateParenthesisDP(n);
System.out.println("\n动态规划结果 (" + result3.size() + " 个):");
for (String s : result3) {
System.out.println(" " + s);
}
System.out.println("\n卡特兰数 C(" + n + ") = " + solution.catalanNumber(n));
System.out.println("直接公式 C(" + n + ") = " + solution.catalanDirect(n));
// 测试更大的 n
int bigN = 10;
System.out.println("\n卡特兰数 C(" + bigN + ") = " + solution.catalanNumber(bigN));
}
}
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 回溯法 | O(4^n / √n) | O(n) | 最优的序列生成方法 |
| 分治法 | O(4^n / √n) | O(n) | 递归思路清晰,但重复计算多 |
| 动态规划 | O(4^n / √n) | O(n × C(n)) | 存储所有中间结果 |
| 卡特兰数 | O(n) | O(1) | 仅计算数量,不生成序列 |
扩展思考
- 限制嵌套深度:生成的序列中括号最大嵌套层数不超过
k。 - 多种括号:同时处理
()、[]、{}的合法组合。 - 最长合法子串:给定一个括号字符串,找出最长的合法连续子串。
总结
括号生成问题通过简单的约束条件(right < left)展示了回溯算法的精髓:在递归的每一步做出选择,并在不满足条件时及时剪枝。同时,它与卡特兰数的深刻联系揭示了许多组合问题的统一结构。掌握这一经典模型,对于理解递归树、剪枝策略和组合计数都有重要意义。