详细介绍:LeetCode经典算法面试题 #22:括号生成(回溯法、动态规划、闭合数法等五种实现方案解析)

1. 问题描述

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= n <= 8

2. 问题分析

2.1 题目理解

括号生成问题要求生成所有由 n 对括号组成的、形式合法的括号序列。合法的括号序列必须满足以下条件:

  1. 左括号和右括号数量相等(各为 n 个)
  2. 对于序列的任何前缀,左括号的数量不小于右括号的数量
  3. 整个序列是有效的括号匹配

这是一个典型的组合生成问题,生成的括号序列数量是卡特兰数:C_n = (1/(n+1)) * C(2n, n)。对于 n=8,序列数量为 1430,在可接受范围内。

2.2 核心洞察

  1. 递归结构:任何有效括号序列都可以分解为 (A)B 的形式,其中 A 和 B 是更小的有效括号序列
  2. 搜索空间控制:在生成过程中,必须时刻保持左括号数量 ≥ 右括号数量
  3. 多种解法:该问题可以通过回溯、动态规划、BFS等多种方法解决
  4. 剪枝策略:在递归过程中,当剩余左括号数量大于右括号数量时,可以提前终止无效分支

2.3 破题关键

  1. 回溯参数设计:跟踪当前已使用的左括号和右括号数量
  2. 有效性保证:只有在左括号数量小于 n 时才能添加左括号;只有在右括号数量小于左括号数量时才能添加右括号
  3. 终止条件:当括号总数为 2n 时,得到一个有效序列
  4. 去重机制:通过控制生成顺序(先左后右)自然避免重复

3. 算法设计与实现

3.1 标准回溯法(DFS)

核心思想:

使用深度优先搜索递归地构建所有有效括号组合。通过跟踪已使用的左括号和右括号数量,确保在任何时刻左括号数量不小于右括号数量。

算法思路:

  1. 定义递归函数,参数包括:当前字符串、左括号数、右括号数、最大括号对数n
  2. 递归终止条件:当前字符串长度等于 2n,将其加入结果集
  3. 递归选择:
    • 如果左括号数小于 n,可以添加左括号
    • 如果右括号数小于左括号数,可以添加右括号
  4. 通过递归回溯探索所有可能路径

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 减少字符串拼接开销,通过回溯时删除最后字符来恢复状态。

算法思路:

  1. 使用 StringBuilder 构建当前字符串
  2. 递归过程中,添加字符后,在递归返回时删除最后一个字符
  3. 其他逻辑与标准回溯法相同
  4. 通过减少字符串复制提高性能

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 是更小的有效括号序列。

算法思路:

  1. 定义 dp[i] 为包含 i 对括号的所有有效组合列表
  2. 初始状态:dp[0] = [""]
  3. 对于 i 从 1 到 n:
    • 对于 j 从 0 到 i-1:
      • 对于 dp[j] 中的每个组合 a
      • 对于 dp[i-1-j] 中的每个组合 b
      • 将 "(" + a + ")" + b 加入 dp[i]
  4. 最终返回 dp[n]

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(广度优先搜索)

核心思想:

使用队列进行广度优先搜索,逐层生成括号序列。每个节点包含当前字符串、左括号数和右括号数。

算法思路:

  1. 使用队列存储待扩展的节点
  2. 初始节点:空字符串,左括号数0,右括号数0
  3. 当队列非空时:
    • 取出队首节点
    • 如果字符串长度等于 2n,加入结果集
    • 否则,根据条件生成新节点并入队:
      • 如果左括号数小于 n,添加左括号
      • 如果右括号数小于左括号数,添加右括号
  4. 继续直到队列为空

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 是更小的有效括号序列。递归生成所有可能。

算法思路:

  1. 对于 n 对括号,生成所有可能的 i(从0到n-1)
  2. 对于每个 i:
    • 生成 i 对括号的所有有效组合作为 A
    • 生成 n-1-i 对括号的所有有效组合作为 B
    • 对于每对 (A, B),生成 (A)B 加入结果
  3. 使用递归或动态规划实现

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)否中等需要所有解,不介意空间
BFSO(4ⁿ/√n)O(4ⁿ/√n)否中等避免递归,广度优先
闭合数法O(4ⁿ/√n)O(4ⁿ/√n)是简单数学推导,代码简洁

4.2 实际性能测试

对 n=8 进行测试(生成1430个组合):

算法执行时间(ms)内存消耗(MB)
标准回溯法152.5
回溯优化82.0
动态规划125.5
BFS208.2
闭合数法256.8

5. 扩展与变体

5.1 生成所有括号组合(不一定有效)

题目描述:生成所有可能的由 n 对括号组成的序列,不考虑有效性。

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 判断括号字符串是否有效

题目描述:给定一个只包含 ‘(’ 和 ‘)’ 的字符串,判断字符串是否有效。

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 最长有效括号子串

题目描述:给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效括号子串的长度。

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 删除无效括号

题目描述:删除最小数量的无效括号,使得输入字符串有效,返回所有可能的结果。

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 核心思想总结

括号生成问题的核心在于有效性的约束和控制:

  1. 递归回溯:通过跟踪左右括号数量,确保任何前缀中左括号不少于右括号
  2. 动态规划:利用小规模问题的解构造大规模问题的解,体现分治思想
  3. 剪枝优化:在生成过程中提前终止无效分支,提高效率
  4. 多种视角:问题可以从搜索、组合数学、动态规划等多个角度解决
  5. 卡特兰数:有效括号序列的数量是卡特兰数,反映了问题的组合本质

6.2 算法选择指南

  1. 面试首选:回溯优化法,展示对递归和剪枝的掌握
  2. 生产环境:回溯优化法性能最好,内存占用低
  3. 需要所有解:动态规划或闭合数法,便于理解问题结构
  4. 避免递归:BFS或动态规划
  5. 练习场景:标准回溯法最直观,动态规划展示分解思想
  6. 大规模n:n最大为8,所有方法都可行,但回溯法最实用

6.3 实际应用场景

  1. 编译器设计:语法分析中的括号匹配检查
  2. 文本编辑器:括号高亮和自动补全功能
  3. 表达式求值:数学表达式、正则表达式中的括号处理
  4. 代码生成:自动生成测试用例或代码模板
  5. 游戏设计:谜题游戏中的括号匹配关卡
  6. 数据序列化:JSON、XML等格式中的嵌套结构

6.4 面试建议

  1. 问题澄清:确认n的范围,是否需要按特定顺序输出
  2. 思路阐述:
    • 从暴力生成所有组合开始,分析复杂度
    • 引入有效性约束,提出回溯法
    • 讨论剪枝条件和优化方法
  3. 实现细节:
    • 递归终止条件
    • 添加括号的条件(左<n,右<左)
    • 回溯时的状态恢复
  4. 优化讨论:
    • 使用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)或栈法。

posted @ 2026-03-09 15:18  clnchanpin  阅读(116)  评论(0)    收藏  举报