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

目录

  • [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 对括号组成的、形式合法的括号序列。合法的括号序列必须满足以下条件:

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

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

2.2 核心洞察

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

2.3 破题关键

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

3. 算法设计与实现

3.1 标准回溯法(DFS)

核心思想

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

算法思路

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

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

算法思路

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

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

算法思路

  1. 定义 dp[i] 为包含 i 对括号的所有有效组合列表
  2. 初始状态:dp[0] = [""]
  3. 对于 i1n
    • 对于 j0i-1
      • 对于 dp[j] 中的每个组合 a
      • 对于 dp[i-1-j] 中的每个组合 b
      • "(" + a + ")" + b 加入 dp[i]
  4. 最终返回 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(广度优先搜索)

核心思想

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

算法思路

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

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

算法思路

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

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

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

  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)或栈法。

相关推荐
寻寻觅觅☆7 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子7 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
化学在逃硬闯CS8 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar1238 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS9 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗9 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
忙什么果10 小时前
上位机、下位机、FPGA、算法放在哪层合适?
算法·fpga开发
董董灿是个攻城狮10 小时前
AI 视觉连载4:YUV 的图像表示
算法
ArturiaZ11 小时前
【day24】
c++·算法·图论
大江东去浪淘尽千古风流人物11 小时前
【SLAM】Hydra-Foundations 层次化空间感知:机器人如何像人类一样理解3D环境
深度学习·算法·3d·机器人·概率论·slam