当前位置: 首页 > news >正文

【LeetCode 热题 100】39. 组合总和——(解法一)选或不选

Problem: 39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(S) 或 指数级
    • 空间复杂度:O(T)

整体思路

这段代码旨在解决一个经典的组合搜索问题:组合总和 (Combination Sum)。问题要求从一个无重复元素的正整数数组 candidates 中,找出所有和为 target 的组合。一个关键的特点是,数组中的同一个数字可以被无限制地重复使用。

该算法采用了 深度优先搜索 (DFS) 结合 回溯 (Backtracking) 的策略来系统地探索所有可能的组合。其核心逻辑步骤如下:

  1. 算法核心:递归与回溯

    • 算法的主体是一个名为 dfs 的递归函数。这个函数负责在 candidates 数组中探索所有可能的数字组合。
    • “路径”与“结果”:算法使用一个 path 列表来存储当前正在构建的组合(即从根节点到当前节点的路径),以及一个 ans 列表来存储所有已找到的、符合条件的最终组合。
  2. DFS函数设计

    • dfs(i, ..., target, ...):这个函数的设计非常关键。
      • i:代表当前递归层级可以从 candidates[i] 开始选择数字。这个参数是为了避免产生重复的组合(例如,有了 [2, 2, 3] 之后,就不再需要生成 [2, 3, 2])。通过只向后(或在原地)选择,我们保证了每个组合中的元素是按非递减顺序被选中的。
      • target:表示还需要凑齐的数值。每当选择一个数字,target 就会相应减少。
  3. 递归的终止条件(Base Cases)

    • 成功找到组合if (target == 0)。当 target 减为 0,说明当前 path 中的数字之和恰好等于原始的 target。这是一个有效的组合,因此需要将其拷贝一份new ArrayList<>(path))存入最终结果 ans 中。必须拷贝,因为 path 本身会在后续的回溯过程中被修改。
    • 路径无效if (i == n || target < 0)。当 i == n,表示已经考虑完了所有候选数字,但仍未凑齐 target;当 target < 0,表示当前组合的和已经超出了 target。这两种情况都说明当前路径是无效的,需要终止这条路的探索,直接 return
  4. 探索与回溯(核心选择逻辑)

    • dfs 函数中,对于当前候选数字 candidates[i],我们面临两个选择:
      a. 不选择 candidates[i]:直接跳过当前数字,去考虑下一个数字 candidates[i+1]。这通过递归调用 dfs(i + 1, ...) 来实现。
      b. 选择 candidates[i]
      i. 做选择:将 candidates[i] 添加到当前路径 path 中。
      ii. 继续探索:进行下一步递归调用 dfs(i, ..., target - candidates[i], ...)。注意,这里的第一个参数仍然是 i,而不是 i+1。这正是允许重复使用同一个数字的关键所在。
      iii. 撤销选择(回溯):当上述递归调用返回后(意味着所有从“选择candidates[i]”开始的路径都已探索完毕),必须将刚刚添加的 candidates[i]path 中移除 (path.removeLast())。这个“撤销”操作是回溯的精髓,它使得程序能够返回到上一个状态,去探索其他的可能性(例如,上一个递归层级的不选择分支)。

完整代码

class Solution {/*** 找出所有和为 target 的组合。candidates 中的数字可以被无限制重复使用。* @param candidates 无重复元素的正整数数组* @param target 目标和* @return 所有有效的组合列表*/public List<List<Integer>> combinationSum(int[] candidates, int target) {// ans: 存储所有符合条件的最终组合List<List<Integer>> ans = new ArrayList<>();// path: 存储当前正在构建的组合(路径)List<Integer> path = new ArrayList<>();// 从索引 0 开始进行深度优先搜索dfs(0, candidates, target, ans, path);return ans;}/*** 深度优先搜索(回溯)辅助函数* @param i 当前考虑的候选数字的索引* @param candidates 候选数字数组* @param target 剩余需要凑齐的目标和* @param ans 结果列表* @param path 当前路径*/private void dfs(int i, int[] candidates, int target, List<List<Integer>> ans, List<Integer> path) {int n = candidates.length;// 递归终止条件 1: 索引越界或目标和变为负数,说明此路不通if (i == n || target < 0) {return;}// 递归终止条件 2: 成功找到一个组合if (target == 0) {// 将当前路径的一个快照(副本)加入结果集// 必须创建新列表,因为 path 会在回溯时被修改ans.add(new ArrayList<>(path));return;}// --- 核心选择逻辑 ---// 选择 1: 不使用当前的 candidates[i],直接考虑下一个数字// 递归地探索不包含当前数字的后续组合dfs(i + 1, candidates, target, ans, path);// 选择 2: 使用当前的 candidates[i]// 2a. 做出选择:将当前数字加入路径path.add(candidates[i]);// 2b. 继续探索:递归调用 dfs。//    - 第一个参数仍为 i,表示可以重复使用当前的 candidates[i]。//    - target 减去当前数字的值。dfs(i, candidates, target - candidates[i], ans, path);// 2c. 撤销选择(回溯):将刚才加入的数字移除,以便探索其他分支path.removeLast();}
}

时空复杂度

时间复杂度:O(S) 或 指数级

  1. 时间复杂度分析对于回溯问题通常比较复杂,因为它取决于搜索树的大小。
  2. Ncandidates 的数量,Ttarget
  3. 搜索树的深度最多为 T / min_val,其中 min_valcandidates 中的最小值。在最坏情况下(例如 candidates 中有1),深度可以是 T
  4. 在搜索树的每个节点,我们都可能进行分支。
  5. 因此,总的节点数(即 dfs 的调用次数)是指数级的。一个非常粗略的上界可以是 O(N^T),但这并不精确。更准确的描述是,时间复杂度与解的数量和搜索空间的大小有关。
  6. 在每个找到解的叶子节点,我们需要 O(T) 的时间来复制 path 到结果列表中。
  7. 一个更被接受的表述是,时间复杂度为 O(S),其中 S 是搜索树中节点的总数。这个 S 的值是输入 NT 的指数函数。

空间复杂度:O(T)

  1. 递归栈深度:空间复杂度的主要部分是递归调用栈的深度。在最坏的情况下,例如 candidates 中有 1,我们需要递归 T 次才能使 target 减到 0。因此,递归栈的最大深度是 O(T)
  2. path 列表path 列表存储了当前路径,其最大长度也与递归深度相关,同样是 O(T)
  3. 结果列表 ans:存储最终结果的空间不计入算法的额外辅助空间复杂度。

综合分析
算法所需的额外空间由递归栈和 path 列表决定。因此,空间复杂度为 O(T)

参考灵神

http://www.dtcms.com/a/295293.html

相关文章:

  • 【物联网】基于树莓派的物联网开发【16】——树莓派GPIO控制LED灯实验
  • 暑期算法训练.7
  • 97.2%灵敏度,桐树基因MSI NGS 2249 Panel——低肿瘤含量MSI检测的王者
  • CIRL:因果启发的表征学习框架——从域泛化到奖励分解的因果革命
  • LLM:Day1
  • 【Linux】linux基础开发工具(一) 软件包管理器yum、编辑器vim使用与相关命令
  • Web前端:JavaScript some()迭代方法
  • 前端如何利用多通道发布(MCP)打造高效AI驱动应用?
  • Hadoop磁盘I/O瓶颈的监控与优化:从iostat指标到JBOD vs RAID的深度解析
  • 2025真实面试试题分析-iOS客户端开发
  • Spring工程中集成多个redis数据源
  • iOS WebView 加载失败与缓存刷新问题排查实战指南
  • [iOS开发工具] 【iOS14以及以下】cydia商店按键精灵iOS新版V2.X安装教程
  • STM32 IAP升级失败之谜:时钟源配置的陷阱与解决方案
  • 【AJAX】XMLHttpRequest、Promise 与 axios的关系
  • JAVA图文短视频交友+自营商城系统源码支持小程序+Android+IOS+H5
  • 【计算机网络】第六章:应用层
  • socket请求
  • 第二十章 W55MH32 WOL示例
  • B站 XMCVE Pwn入门课程学习笔记(5)
  • windows11通过wsl安装Ubuntu到D盘,安装docker及宝塔面板
  • 如何彻底清除服务器上的恶意软件与后门
  • 基础入门 [CMD] Windows SSH 连接服务器教程(系统自带方式)
  • Linux 下在线安装启动VNC
  • TCP 套接字--服务器相关
  • 杰理通用MCU串口+AT指令+485通讯工业语音芯片
  • PostgreSQL 跨库查询方法
  • Apache Flink 实时流处理性能优化实践指南
  • uniapp写app做测试手机通知栏展示内容
  • uni-appDay02