LeetCode题练习与总结:组合总和

169 篇文章 0 订阅
106 篇文章 0 订阅

一、题目描述

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

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

二、解题思路

1. 排序:首先对candidates数组进行排序,这样可以在后续的回溯过程中进行剪枝,避免重复选择相同的数字。

2. 递归:使用递归函数,每次选择一个候选数字,并从数组中移除(或者标记已使用),然后递归地寻找剩余目标值的组合。

3. 剪枝

  • 当前候选数字与上一个数字相同,并且当前路径的和已经达到了目标值,说明这是一个重复的组合,直接跳过这次递归。
  • 如果当前候选数字大于剩余的目标值,说明无法再添加这个数字,直接返回。

4. 记录结果:当递归到目标值减至0时,将当前的组合添加到结果列表中。

5. 回溯:在递归结束后,将当前的候选数字放回原位(如果是从数组中移除的话),以便它可以被上一级递归使用。

三、具体代码

import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> results = new ArrayList<>();
        if (candidates == null || candidates.length == 0) {
            return results;
        }
        
        // 对数组进行排序
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, new ArrayList<>(), results);
        return results;
    }

    private void backtrack(int[] candidates, int remain, int start, List<Integer> combination, List<List<Integer>> results) {
        if (remain < 0) {
            // 如果当前剩余值小于0,则说明当前组合无法形成解,直接返回
            return;
        } else if (remain == 0) {
            // 如果当前剩余值为0,则说明找到了一个有效的组合,将其添加到结果中
            results.add(new ArrayList<>(combination));
            return;
        }

        for (int i = start; i < candidates.length; i++) {
            // 剪枝:如果当前数字和上一个数字相同,且已经达到目标值,则跳过
            if (i > start && candidates[i] == candidates[i - 1]) {
                continue;
            }

            // 选择当前数字,并递归寻找剩余值的组合
            combination.add(candidates[i]);
            backtrack(candidates, remain - candidates[i], i, combination, results);

            // 回溯:将当前数字从组合中移除,以便它可以被上一级递归使用
            combination.remove(combination.size() - 1);
        }
    }
}

四、时间复杂度和空间复杂度

1. 时间复杂度
  • 排序:对candidates数组进行排序的时间复杂度是O(n log n),其中n是数组的长度。

  • 回溯搜索:在最坏情况下,回溯算法会探索所有可能的组合。对于每个候选数字,我们有两种选择:包含它或者不包含它。因此,对于每个数字,我们都需要进行一次完整的递归搜索。如果数组中的数字可以重复使用,那么对于每个数字,我们可能会重复探索相同的子问题。在最坏的情况下,这会导致时间复杂度接近O(2^n),其中n是数组中不同数字的种类数。然而,由于我们在每一步都尽可能地减少搜索空间(通过剪枝),实际的执行时间通常会比这个最坏情况要好得多。

  • 结果集合:我们需要将所有有效的组合存储在结果集合中,最多可能有少于150个组合,所以这部分的时间复杂度是O(k),其中k是有效组合的数量。

  • 综上所述,算法的总体时间复杂度由排序和回溯搜索两部分决定。排序是一次性的,而回溯搜索的时间复杂度可能会非常高,但由于剪枝,实际执行时间通常不会达到最坏情况的O(2^n)。结果集合的构建时间复杂度是O(k),其中k是有效组合的数量。

  • 因此,我们可以大致估计算法的时间复杂度为O(n log n + k),其中n是数组长度,k是有效组合的数量。

2. 空间复杂度
  • 递归栈:在最坏情况下,递归的深度可以达到数组长度,因为我们需要对每个数字进行选择或不选择的决策。因此,递归栈的空间复杂度是O(n)。

  • 结果集合:我们需要存储所有有效的组合,所以结果集合的空间复杂度是O(k),其中k是有效组合的数量。

  • 综上所述,算法的总体空间复杂度取决于递归栈的深度和结果集合的大小,即O(n + k)。

五、总结知识点

  1. 数组排序:使用Arrays.sort方法对输入的整数数组进行排序。这是为了在后续的回溯过程中能够更有效地进行剪枝操作。

  2. 递归backtrack函数是一个递归函数,它模拟了组合问题的求解过程。递归是一种通过将问题分解为更小的子问题来解决问题的方法。

  3. 回溯:回溯是递归的一种特殊形式,它不仅涉及深入问题空间,还涉及从问题空间中退出。在这个问题中,当发现当前的部分解不可能导致最终解时,算法会“回溯”到上一个决策点,尝试其他的选择。

  4. 剪枝:为了避免无效搜索,代码中使用了剪枝技术。在这个问题中,如果当前候选数字与上一个数字相同,并且已经达到目标值,或者如果当前数字大于剩余的目标值,就会跳过这次递归,从而避免了不必要的搜索。

  5. 动态规划的思想:尽管代码中没有直接使用动态规划,但是回溯算法在某种意义上与动态规划有相似之处,特别是在处理组合问题时。两者都试图通过构建问题的解的结构来解决问题。

  6. 数据结构:代码中使用了ArrayList来存储中间结果和最终结果。ArrayList是一种动态数组,可以在运行时动态地添加和删除元素。

  7. 条件判断:在backtrack函数中,通过if语句进行条件判断,以确定是否应该继续搜索或者返回。

  8. List的不可变性:在将组合添加到结果列表results时,代码创建了一个新的ArrayList,这是为了避免修改原始的combination列表,因为List在Java中是不可变的。

  9. 空间复杂度优化:通过在回溯过程中从combination列表中移除元素而不是创建新的列表,优化了空间复杂度。

以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直学习永不止步

谢谢您的鼓励,我会再接再厉的!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值