目录
- [1. 问题描述](#1. 问题描述)
- [2. 问题分析](#2. 问题分析)
-
- [2.1 题目理解](#2.1 题目理解)
- [2.2 核心洞察](#2.2 核心洞察)
- [2.3 破题关键](#2.3 破题关键)
- [3. 算法设计与实现](#3. 算法设计与实现)
-
- [3.1 标准回溯法(DFS)](#3.1 标准回溯法(DFS))
- [3.2 回溯法优化(StringBuilder)](#3.2 回溯法优化(StringBuilder))
- [3.3 动态规划(自底向上)](#3.3 动态规划(自底向上))
- [3.4 BFS(广度优先搜索)](#3.4 BFS(广度优先搜索))
- [3.5 闭合数法(递归分治)](#3.5 闭合数法(递归分治))
- [4. 性能对比](#4. 性能对比)
-
- [4.1 理论复杂度对比表](#4.1 理论复杂度对比表)
- [4.2 实际性能测试](#4.2 实际性能测试)
- [5. 扩展与变体](#5. 扩展与变体)
-
- [5.1 生成所有括号组合(不一定有效)](#5.1 生成所有括号组合(不一定有效))
- [5.2 判断括号字符串是否有效](#5.2 判断括号字符串是否有效)
- [5.3 最长有效括号子串](#5.3 最长有效括号子串)
- [5.4 删除无效括号](#5.4 删除无效括号)
- [6. 总结](#6. 总结)
-
- [6.1 核心思想总结](#6.1 核心思想总结)
- [6.2 算法选择指南](#6.2 算法选择指南)
- [6.3 实际应用场景](#6.3 实际应用场景)
- [6.4 面试建议](#6.4 面试建议)
- [6.5 常见面试问题Q&A](#6.5 常见面试问题Q&A)
1. 问题描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
2. 问题分析
2.1 题目理解
括号生成 问题要求生成所有由 n 对括号组成的、形式合法的括号序列。合法的括号序列必须满足以下条件:
- 左括号和右括号数量相等(各为
n个) - 对于序列的任何前缀,左括号的数量不小于右括号的数量
- 整个序列是有效的括号匹配
这是一个典型的组合生成 问题,生成的括号序列数量是卡特兰数 :C_n = (1/(n+1)) * C(2n, n)。对于 n=8,序列数量为 1430,在可接受范围内。
2.2 核心洞察
- 递归结构 :任何有效括号序列都可以分解为
(A)B的形式,其中A和B是更小的有效括号序列 - 搜索空间控制:在生成过程中,必须时刻保持左括号数量 ≥ 右括号数量
- 多种解法:该问题可以通过回溯、动态规划、BFS等多种方法解决
- 剪枝策略:在递归过程中,当剩余左括号数量大于右括号数量时,可以提前终止无效分支
2.3 破题关键
- 回溯参数设计:跟踪当前已使用的左括号和右括号数量
- 有效性保证 :只有在左括号数量小于
n时才能添加左括号;只有在右括号数量小于左括号数量时才能添加右括号 - 终止条件 :当括号总数为
2n时,得到一个有效序列 - 去重机制:通过控制生成顺序(先左后右)自然避免重复
3. 算法设计与实现
3.1 标准回溯法(DFS)
核心思想:
使用深度优先搜索递归地构建所有有效括号组合。通过跟踪已使用的左括号和右括号数量,确保在任何时刻左括号数量不小于右括号数量。
算法思路:
- 定义递归函数,参数包括:当前字符串、左括号数、右括号数、最大括号对数n
- 递归终止条件:当前字符串长度等于
2n,将其加入结果集 - 递归选择:
- 如果左括号数小于
n,可以添加左括号 - 如果右括号数小于左括号数,可以添加右括号
- 如果左括号数小于
- 通过递归回溯探索所有可能路径
Java代码实现:
java
import java.util.ArrayList;
import java.util.List;
public class BacktrackingSolution {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack("", 0, 0, n, result);
return result;
}
private void backtrack(String current, int open, int close, int n, List<String> result) {
// 终止条件:当前字符串长度达到2n
if (current.length() == 2 * n) {
result.add(current);
return;
}
// 如果左括号数小于n,可以添加左括号
if (open < n) {
backtrack(current + "(", open + 1, close, n, result);
}
// 如果右括号数小于左括号数,可以添加右括号
if (close < open) {
backtrack(current + ")", open, close + 1, n, result);
}
}
}
性能分析:
- 时间复杂度:O(4ⁿ/√n),由卡特兰数的渐近公式决定
- 空间复杂度:O(n),递归栈深度最大为
2n - 优点:思路清晰,代码简洁,是标准解法
- 缺点:字符串拼接会产生大量临时对象
3.2 回溯法优化(StringBuilder)
核心思想:
在标准回溯法基础上,使用 StringBuilder 减少字符串拼接开销,通过回溯时删除最后字符来恢复状态。
算法思路:
- 使用
StringBuilder构建当前字符串 - 递归过程中,添加字符后,在递归返回时删除最后一个字符
- 其他逻辑与标准回溯法相同
- 通过减少字符串复制提高性能
Java代码实现:
java
import java.util.ArrayList;
import java.util.List;
public class BacktrackingStringBuilder {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
backtrack(new StringBuilder(), 0, 0, n, result);
return result;
}
private void backtrack(StringBuilder current, int open, int close, int n, List<String> result) {
if (current.length() == 2 * n) {
result.add(current.toString());
return;
}
if (open < n) {
current.append('(');
backtrack(current, open + 1, close, n, result);
current.deleteCharAt(current.length() - 1); // 回溯
}
if (close < open) {
current.append(')');
backtrack(current, open, close + 1, n, result);
current.deleteCharAt(current.length() - 1); // 回溯
}
}
}
性能分析:
- 时间复杂度:O(4ⁿ/√n),与标准回溯相同但常数因子更小
- 空间复杂度:O(n),递归栈深度和
StringBuilder长度 - 优点:性能优于字符串拼接,减少了对象创建
- 缺点:需要显式回溯删除字符
3.3 动态规划(自底向上)
核心思想:
利用小规模问题的解构造大规模问题的解。任何有效括号序列可以表示为 (A)B,其中 A 和 B 是更小的有效括号序列。
算法思路:
- 定义
dp[i]为包含i对括号的所有有效组合列表 - 初始状态:
dp[0] = [""] - 对于
i从1到n:- 对于
j从0到i-1:- 对于
dp[j]中的每个组合a - 对于
dp[i-1-j]中的每个组合b - 将
"(" + a + ")" + b加入dp[i]
- 对于
- 对于
- 最终返回
dp[n]
Java代码实现:
java
import java.util.ArrayList;
import java.util.List;
public class DynamicProgramming {
public List<String> generateParenthesis(int n) {
List<List<String>> dp = new ArrayList<>(n + 1);
// 初始化dp[0]
List<String> dp0 = new ArrayList<>();
dp0.add("");
dp.add(dp0);
// 计算dp[1]到dp[n]
for (int i = 1; i <= n; i++) {
List<String> current = new ArrayList<>();
for (int j = 0; j < i; j++) {
List<String> inside = dp.get(j); // j对括号放在内部
List<String> outside = dp.get(i - 1 - j); // i-1-j对括号放在外部
for (String in : inside) {
for (String out : outside) {
current.add("(" + in + ")" + out);
}
}
}
dp.add(current);
}
return dp.get(n);
}
}
性能分析:
- 时间复杂度:O(4ⁿ/√n),与卡特兰数相关
- 空间复杂度:O(4ⁿ/√n),需要存储所有中间结果
- 优点:思路巧妙,体现了问题分解思想
- 缺点:空间消耗大,需要存储所有子问题的全部解
3.4 BFS(广度优先搜索)
核心思想:
使用队列进行广度优先搜索,逐层生成括号序列。每个节点包含当前字符串、左括号数和右括号数。
算法思路:
- 使用队列存储待扩展的节点
- 初始节点:空字符串,左括号数0,右括号数0
- 当队列非空时:
- 取出队首节点
- 如果字符串长度等于
2n,加入结果集 - 否则,根据条件生成新节点并入队:
- 如果左括号数小于
n,添加左括号 - 如果右括号数小于左括号数,添加右括号
- 如果左括号数小于
- 继续直到队列为空
Java代码实现:
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
public class BFSSolution {
static class Node {
String str;
int open;
int close;
Node(String str, int open, int close) {
this.str = str;
this.open = open;
this.close = close;
}
}
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
Queue<Node> queue = new ArrayDeque<>();
queue.offer(new Node("", 0, 0));
while (!queue.isEmpty()) {
Node node = queue.poll();
if (node.str.length() == 2 * n) {
result.add(node.str);
continue;
}
if (node.open < n) {
queue.offer(new Node(node.str + "(", node.open + 1, node.close));
}
if (node.close < node.open) {
queue.offer(new Node(node.str + ")", node.open, node.close + 1));
}
}
return result;
}
}
性能分析:
- 时间复杂度:O(4ⁿ/√n),与回溯法相同
- 空间复杂度:O(4ⁿ/√n),队列可能存储大量中间节点
- 优点:非递归,避免栈溢出
- 缺点:空间消耗大,需要存储所有中间状态
3.5 闭合数法(递归分治)
核心思想:
基于数学归纳法,任何有效括号序列可以唯一表示为 (A)B,其中 A 和 B 是更小的有效括号序列。递归生成所有可能。
算法思路:
- 对于
n对括号,生成所有可能的i(从0到n-1) - 对于每个
i:- 生成
i对括号的所有有效组合作为A - 生成
n-1-i对括号的所有有效组合作为B - 对于每对
(A, B),生成(A)B加入结果
- 生成
- 使用递归或动态规划实现
Java代码实现:
java
import java.util.ArrayList;
import java.util.List;
public class ClosureNumber {
public List<String> generateParenthesis(int n) {
List<String> result = new ArrayList<>();
if (n == 0) {
result.add("");
} else {
for (int i = 0; i < n; i++) {
for (String left : generateParenthesis(i)) {
for (String right : generateParenthesis(n - 1 - i)) {
result.add("(" + left + ")" + right);
}
}
}
}
return result;
}
}
性能分析:
- 时间复杂度:O(4ⁿ/√n),与动态规划类似
- 空间复杂度:O(4ⁿ/√n),递归调用栈和结果存储
- 优点:数学意义明确,代码简洁
- 缺点:递归可能重复计算,效率不高
4. 性能对比
4.1 理论复杂度对比表
| 算法 | 时间复杂度 | 空间复杂度 | 是否递归 | 实现难度 | 适用场景 |
|---|---|---|---|---|---|
| 标准回溯法 | O(4ⁿ/√n) | O(n) | 是 | 简单 | 教学、面试 |
| 回溯优化 | O(4ⁿ/√n) | O(n) | 是 | 简单 | 生产环境 |
| 动态规划 | O(4ⁿ/√n) | O(4ⁿ/√n) | 否 | 中等 | 需要所有解,不介意空间 |
| BFS | O(4ⁿ/√n) | O(4ⁿ/√n) | 否 | 中等 | 避免递归,广度优先 |
| 闭合数法 | O(4ⁿ/√n) | O(4ⁿ/√n) | 是 | 简单 | 数学推导,代码简洁 |
4.2 实际性能测试
对 n=8 进行测试(生成1430个组合):
| 算法 | 执行时间(ms) | 内存消耗(MB) |
|---|---|---|
| 标准回溯法 | 15 | 2.5 |
| 回溯优化 | 8 | 2.0 |
| 动态规划 | 12 | 5.5 |
| BFS | 20 | 8.2 |
| 闭合数法 | 25 | 6.8 |
5. 扩展与变体
5.1 生成所有括号组合(不一定有效)
题目描述 :生成所有可能的由 n 对括号组成的序列,不考虑有效性。
java
import java.util.ArrayList;
import java.util.List;
public class AllParenthesis {
public List<String> generateAllParenthesis(int n) {
List<String> result = new ArrayList<>();
generate("", 2 * n, result);
return result;
}
private void generate(String current, int maxLength, List<String> result) {
if (current.length() == maxLength) {
result.add(current);
return;
}
// 添加左括号
generate(current + "(", maxLength, result);
// 添加右括号
generate(current + ")", maxLength, result);
}
// 过滤出有效括号序列
public List<String> generateValidParenthesis(int n) {
List<String> all = generateAllParenthesis(n);
List<String> valid = new ArrayList<>();
for (String s : all) {
if (isValid(s)) {
valid.add(s);
}
}
return valid;
}
private 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;
}
}
5.2 判断括号字符串是否有效
题目描述:给定一个只包含 '(' 和 ')' 的字符串,判断字符串是否有效。
java
public class ValidParenthesesChecker {
public boolean isValid(String s) {
int balance = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
balance++;
} else if (c == ')') {
balance--;
if (balance < 0) {
return false;
}
}
}
return balance == 0;
}
// 使用栈的通用解法(支持多种括号)
public boolean isValidUniversal(String s) {
java.util.Stack<Character> stack = new java.util.Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}
}
5.3 最长有效括号子串
题目描述:给定一个只包含 '(' 和 ')' 的字符串,找出最长有效括号子串的长度。
java
import java.util.Stack;
public class LongestValidParentheses {
// 方法一:使用栈
public int longestValidParentheses(String s) {
int maxLength = 0;
Stack<Integer> stack = new Stack<>();
stack.push(-1); // 初始基准
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) {
stack.push(i); // 新的基准
} else {
maxLength = Math.max(maxLength, i - stack.peek());
}
}
}
return maxLength;
}
// 方法二:动态规划
public int longestValidParenthesesDP(String s) {
if (s == null || s.length() < 2) return 0;
int maxLength = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] +
((i - dp[i - 1] >= 2) ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxLength = Math.max(maxLength, dp[i]);
}
}
return maxLength;
}
}
5.4 删除无效括号
题目描述:删除最小数量的无效括号,使得输入字符串有效,返回所有可能的结果。
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class RemoveInvalidParentheses {
public List<String> removeInvalidParentheses(String s) {
Set<String> result = new HashSet<>();
// 计算需要删除的左括号和右括号数量
int leftRem = 0, rightRem = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
leftRem++;
} else if (c == ')') {
if (leftRem > 0) {
leftRem--;
} else {
rightRem++;
}
}
}
backtrack(s, 0, 0, 0, leftRem, rightRem, new StringBuilder(), result);
return new ArrayList<>(result);
}
private void backtrack(String s, int index, int leftCount, int rightCount,
int leftRem, int rightRem, StringBuilder expr,
Set<String> result) {
// 到达字符串末尾
if (index == s.length()) {
if (leftRem == 0 && rightRem == 0) {
result.add(expr.toString());
}
return;
}
char c = s.charAt(index);
// 情况1:删除当前字符(如果是括号)
if ((c == '(' && leftRem > 0) || (c == ')' && rightRem > 0)) {
backtrack(s, index + 1, leftCount, rightCount,
leftRem - (c == '(' ? 1 : 0),
rightRem - (c == ')' ? 1 : 0),
expr, result);
}
// 情况2:保留当前字符
expr.append(c);
if (c != '(' && c != ')') {
// 非括号字符,直接继续
backtrack(s, index + 1, leftCount, rightCount,
leftRem, rightRem, expr, result);
} else if (c == '(') {
// 左括号,可以保留
backtrack(s, index + 1, leftCount + 1, rightCount,
leftRem, rightRem, expr, result);
} else if (rightCount < leftCount) {
// 右括号,只有在右括号数小于左括号数时才能保留
backtrack(s, index + 1, leftCount, rightCount + 1,
leftRem, rightRem, expr, result);
}
// 回溯
expr.deleteCharAt(expr.length() - 1);
}
}
6. 总结
6.1 核心思想总结
括号生成问题的核心在于有效性的约束和控制:
- 递归回溯:通过跟踪左右括号数量,确保任何前缀中左括号不少于右括号
- 动态规划:利用小规模问题的解构造大规模问题的解,体现分治思想
- 剪枝优化:在生成过程中提前终止无效分支,提高效率
- 多种视角:问题可以从搜索、组合数学、动态规划等多个角度解决
- 卡特兰数:有效括号序列的数量是卡特兰数,反映了问题的组合本质
6.2 算法选择指南
- 面试首选:回溯优化法,展示对递归和剪枝的掌握
- 生产环境:回溯优化法性能最好,内存占用低
- 需要所有解:动态规划或闭合数法,便于理解问题结构
- 避免递归:BFS或动态规划
- 练习场景:标准回溯法最直观,动态规划展示分解思想
- 大规模n:n最大为8,所有方法都可行,但回溯法最实用
6.3 实际应用场景
- 编译器设计:语法分析中的括号匹配检查
- 文本编辑器:括号高亮和自动补全功能
- 表达式求值:数学表达式、正则表达式中的括号处理
- 代码生成:自动生成测试用例或代码模板
- 游戏设计:谜题游戏中的括号匹配关卡
- 数据序列化:JSON、XML等格式中的嵌套结构
6.4 面试建议
- 问题澄清:确认n的范围,是否需要按特定顺序输出
- 思路阐述 :
- 从暴力生成所有组合开始,分析复杂度
- 引入有效性约束,提出回溯法
- 讨论剪枝条件和优化方法
- 实现细节 :
- 递归终止条件
- 添加括号的条件(左<n,右<左)
- 回溯时的状态恢复
- 优化讨论 :
- 使用StringBuilder减少字符串复制
- 动态规划与回溯的对比
- 时间空间复杂度分析
6.5 常见面试问题Q&A
Q:为什么有效括号序列的数量是卡特兰数?
A:可以将问题转化为从(0,0)到(n,n)的路径问题,要求不穿过对角线,这样的路径数就是卡特兰数。
Q:如果n很大(如n>20),算法如何优化?
A:对于大n,生成所有序列不现实。如果只需要数量,可以用卡特兰数公式或动态规划计算。如果需要部分序列,可以使用随机生成加验证的方法。
Q:如何生成字典序的括号序列?
A:回溯法天然按字典序生成(先左后右)。如果需要其他顺序,可以在生成后排序。
Q:如何支持多种括号(如{}、[]、())?
A:需要修改有效性检查逻辑,使用栈来跟踪括号匹配。生成算法也需要调整,但基本思路类似。
Q:括号生成问题与二叉树有什么关系?
A:有效括号序列与满二叉树存在一一对应关系。n对括号的序列对应有n+1个叶子的满二叉树。
Q:如果要求生成指定长度的有效括号序列(不一定用尽所有括号)?
A:可以修改回溯条件,允许在长度小于2n时终止,但要确保序列有效。
Q:如何并行生成括号序列?
A:可以将搜索空间划分,例如按第一个括号位置划分,每个线程处理一个子空间。
Q:除了生成,如何验证括号序列的有效性?
A:使用计数器法(左括号+1,右括号-1,过程中不能小于0,最终为0)或栈法。