LeetCode 中等难度 | 回溯法经典题解:组合总和 & 括号生成

目录

[一、组合总和(LeetCode 39)](#一、组合总和(LeetCode 39))

题目描述

解题思路

[Java 代码实现](#Java 代码实现)

复杂度分析

[易错点 & 优化点](#易错点 & 优化点)

[二、括号生成(LeetCode 22)](#二、括号生成(LeetCode 22))

题目描述

解题思路

[Java 代码实现](#Java 代码实现)

复杂度分析

[易错点 & 优化点](#易错点 & 优化点)

[三、回溯法通用模板 & 核心思想](#三、回溯法通用模板 & 核心思想)

四、总结


一、组合总和(LeetCode 39)

题目描述

给你一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target所有不同组合,并以列表形式返回。你可以按任意顺序返回这些组合。

  • candidates 中的同一个数字可以无限制重复被选取
  • 如果至少一个数字的被选数量不同,则两种组合是不同的。
  • 对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例:

plaintext

复制代码
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]

解题思路

这道题的核心是回溯法,本质是暴力枚举所有可能的组合,再通过剪枝避免无效搜索:

  1. 状态定义 :当前组合的和 sum、当前遍历的数组索引 index、当前组合 path
  2. 终止条件
    • sum == target 时,将当前组合加入结果集;
    • sum > target 时,直接返回(剪枝,避免继续搜索更大的数)。
  3. 递归逻辑
    • 因为元素可重复选,所以每次递归可以选择当前元素(索引不变)或跳过当前元素(索引 + 1);
    • 为了避免重复组合(如 [2,3] 和 [3,2]),我们规定只能按数组顺序向后选元素 ,即每次递归从 index 开始遍历,不回头选前面的元素。

Java 代码实现

java

运行

复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CombinationSum {
    // 结果集:存储所有符合条件的组合
    List<List<Integer>> result = new ArrayList<>();
    // 路径:记录当前递归中的组合
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 先对数组排序,方便后续剪枝(遇到比target大的数可以直接跳过)
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, 0);
        return result;
    }

    /**
     * 回溯函数
     * @param candidates 候选数组
     * @param target 目标和
     * @param index 当前遍历的数组索引(避免重复组合)
     * @param sum 当前组合的和
     */
    private void backtrack(int[] candidates, int target, int index, int sum) {
        // 终止条件1:当前和等于目标和,将路径加入结果集
        if (sum == target) {
            result.add(new ArrayList<>(path));
            return;
        }

        // 终止条件2:当前和大于目标和,直接返回(剪枝)
        if (sum > target) {
            return;
        }

        // 从index开始遍历,避免选前面的元素导致重复组合
        for (int i = index; i < candidates.length; i++) {
            int num = candidates[i];
            // 剪枝:如果当前数加上sum已经超过target,后面的数更大,直接break
            if (sum + num > target) {
                break;
            }

            // 做选择:将当前数加入路径
            path.add(num);
            // 递归:因为元素可重复选,所以下一次递归的index还是i
            backtrack(candidates, target, i, sum + num);
            // 撤销选择:回溯,移除路径最后一个元素
            path.remove(path.size() - 1);
        }
    }
}

复杂度分析

  • 时间复杂度:O(S),其中 S 是所有可行解的长度之和。每个组合的生成需要遍历所有元素,且数组排序的时间为 O(nlogn),整体由可行解数量决定。
  • 空间复杂度:O(target),递归栈的深度最多为 target/min(candidates)(每次选最小的数),路径存储的最大长度也为该值。

易错点 & 优化点

  1. 重复组合问题 :必须通过 index 控制遍历起点,否则会出现 [2,3][3,2] 这样的重复结果;
  2. 剪枝优化 :对数组排序后,当 sum + candidates[i] > target 时,直接 break,无需遍历后面更大的数,大幅减少无效递归;
  3. 路径拷贝 :将 path 加入结果集时,必须 new ArrayList<>(path),否则后续修改 path 会影响结果集中的引用。

二、括号生成(LeetCode 22)

题目描述

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

示例:

plaintext

复制代码
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

解题思路

这道题是回溯法的经典应用,核心是通过控制左右括号的数量,生成有效的括号组合:

  1. 状态定义 :当前字符串 sb、已使用的左括号数 left、已使用的右括号数 right
  2. 终止条件 :当 left == nright == n 时,当前字符串是一个有效组合,加入结果集。
  3. 递归逻辑
    • 左括号的使用数量不能超过 n(最多有 n 个左括号);
    • 右括号的使用数量不能超过左括号(否则会出现 )( 这样的无效组合);
    • 每次递归可以选择添加左括号或右括号(满足条件时),递归结束后回溯(删除最后添加的括号)。

Java 代码实现

java

运行

复制代码
import java.util.ArrayList;
import java.util.List;

public class GenerateParenthesis {
    List<String> result = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    public List<String> generateParenthesis(int n) {
        backtrack(n, 0, 0);
        return result;
    }

    /**
     * 回溯函数
     * @param n 括号对数
     * @param left 已使用的左括号数
     * @param right 已使用的右括号数
     */
    private void backtrack(int n, int left, int right) {
        // 终止条件:左右括号都用完了,将当前字符串加入结果集
        if (left == n && right == n) {
            result.add(sb.toString());
            return;
        }

        // 情况1:添加左括号(左括号数 < n)
        if (left < n) {
            sb.append('(');
            backtrack(n, left + 1, right);
            sb.deleteCharAt(sb.length() - 1); // 回溯:删除最后一个'('
        }

        // 情况2:添加右括号(右括号数 < 左括号数)
        if (right < left) {
            sb.append(')');
            backtrack(n, left, right + 1);
            sb.deleteCharAt(sb.length() - 1); // 回溯:删除最后一个')'
        }
    }
}

复杂度分析

  • 时间复杂度:O(n4n),这是卡特兰数的时间复杂度,生成所有有效括号组合的数量为第 n 个卡特兰数,每个组合的生成需要遍历 2n 个字符。
  • 空间复杂度 :O(n),递归栈的深度为 2n(最多添加 2n 个括号),StringBuilder 的最大长度也为 2n。

易错点 & 优化点

  1. 有效条件控制 :右括号的使用数必须小于左括号,否则会生成无效括号(如 )());
  2. 回溯操作:每次添加括号后,递归结束必须删除,否则会影响后续递归的字符串状态;
  3. 字符串拼接 :使用 StringBuilder 比直接拼接字符串更高效,减少对象创建开销。

三、回溯法通用模板 & 核心思想

这两道题都用到了回溯法,核心逻辑可以总结为以下模板:

java

运行

复制代码
void backtrack(参数) {
    // 1. 终止条件:满足条件时,将当前路径加入结果集
    if (终止条件) {
        result.add(new ArrayList<>(path));
        return;
    }

    // 2. 遍历所有可能的选择
    for (选择 : 选择列表) {
        // 3. 剪枝:排除无效选择
        if (无效条件) continue;

        // 4. 做选择:将当前选择加入路径
        path.add(选择);
        // 5. 递归:进入下一层决策
        backtrack(参数更新);
        // 6. 撤销选择:回溯,移除路径最后一个元素
        path.remove(path.size() - 1);
    }
}

回溯法的核心是 **"暴力枚举 + 剪枝优化"**,通过递归枚举所有可能的解,再通过终止条件和剪枝避免无效搜索,是解决组合、排列、子集等问题的常用方法。


四、总结

  • 组合总和 :重点是通过 index 控制遍历起点避免重复,利用数组排序进行剪枝;
  • 括号生成:重点是通过左右括号的数量限制,生成有效括号组合;
  • 两道题的核心都是回溯法,掌握模板后,很多类似题目(如组合、子集、排列)都可以套用这个思路解决。
相关推荐
im_AMBER2 小时前
Leetcode 153 课程表 | 腐烂的橘子
开发语言·算法·leetcode·深度优先·图搜索
paeamecium2 小时前
【PAT甲级真题】- Reversing Linked List (25)
数据结构·c++·算法·pat
田梓燊2 小时前
leetcode 73
算法·leetcode·职场和发展
ZPC82102 小时前
相机接入ROS2 流程及问题排查
人工智能·算法·机器人
2501_940315262 小时前
【无标题】两个相同字符串中不同字符的个数
算法·哈希算法·散列表
算法鑫探2 小时前
显示器插座最短连线算法(蓝桥杯十六届C组编程题第二题)
c语言·数据结构·算法·排序算法·新人首发
akarinnnn2 小时前
【DAY15】:深⼊理解指针(6)
算法
Lauren_Blueblue3 小时前
第十六届蓝桥杯省赛Python研究生组-C变换数组
python·算法·蓝桥杯·编程基础
生信研究猿3 小时前
leetcode 101.对称二叉树(不会做)
算法·leetcode·职场和发展