组合总和
⛅前言
大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!
精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、字典树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。
博客主页💖:知识汲取者的博客
LeetCode热题100专栏🚀:LeetCode热题100
Gitee地址📁:知识汲取者 (aghp) - Gitee.com
Github地址📁:Chinafrfq · GitHub
题目来源📢:LeetCode 热题 100 - 学习计划 - 力扣(LeetCode)全球极客挚爱的技术成长平台
PS:作者水平有限,如有错误或描述不当的地方,恳请及时告诉作者,作者将不胜感激
🔒题目
原题链接:39. 组合总和 - 力扣(LeetCode)
🔑题解
-
解法一:回溯+剪枝
这个解法,应该是最容易想到的,有一点比较肯,当满足题意的时候,一定要通过new一个新对象加入到ans中!
import java.util.*; import java.util.stream.Collectors; /** * @author ghp * @title 组合总和 */ class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> ans = new ArrayList<>(10); List<Integer> path = new ArrayList<>(10); dfs(ans, path, candidates, target); // 去重 for (List<Integer> list : ans) { Collections.sort(list); } Set<List<Integer>> set = ans.stream().collect(Collectors.toSet()); ans = new ArrayList<>(set); return ans; } private void dfs(List<List<Integer>> ans, List<Integer> path, int[] candidates, int target) { if (target < 0) { // 剪枝 return; } if (target == 0) { // target==0,符合题意,直接将遍历的路径添加到ans中(一定要new一个新对象) ans.add(new ArrayList<>(path)); } for (int i = 0; i < candidates.length; i++) { if (target - candidates[i] < 0){ // 当前不符合题意,直接下一个 continue; } target -= candidates[i]; path.add(candidates[i]); dfs(ans, path, candidates, target); target += candidates[i]; path.remove(path.size() - 1); } } }
复杂度分析:
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( n ) O(n) O(n)
其中 n n n 为数组中元素的个数
备注:这里只是给出一个大概的时间复杂度(我估算的)
经过提交发现,在所有Java提交中,排名特别靠后,所以这段代码肯定是可以优化的,优化代码如下:
这里主要有两个优化点:
- 数据结构的优化:原来的path是ArrayList,现在的path是Deque。因为Deque进行删除和新增操作的耗时要远远低于ArrayList(Deque删除和新增的时间复杂度是 O ( 1 ) O(1) O(1),而ArrayList的时间复杂度是 O ( n ) O(n) O(n))
- 剪枝优化:之前剪枝是在for循环外面实现的,剪枝不够彻底,会多遍历一层无用的数据。现在直接先对待遍历的元素进行排序,然后直接在for循环中进行剪枝,一下就提前将一些无用数据给剪枝了,这样剪枝更加彻底
- 代码逻辑优化:前面我们每次递归,都需要重新枚举所有元素的种类,现在我们每次递归都传递当前层,这样就能避免下次递归时,遍历到重复的元素了,从而有效避免了元素重复,也减少了去重的时间和空间损耗
之前剪枝在for循环外面,会多遍历一层无用的数据,增加时间损耗:
import java.util.*; /** * @author ghp * @title 组合总和 */ class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> ans = new ArrayList<>(10); Deque<Integer> path = new LinkedList<>(); // 对待选元素进行排序(升序),这是剪枝的前提 Arrays.sort(candidates); dfs(ans, path, candidates, 0, target); return ans; } private void dfs(List<List<Integer>> ans, Deque<Integer> path, int[] candidates, int step, int target) { if (target == 0) { ans.add(new ArrayList<>(path)); return; } for (int i = step; i < candidates.length; i++) { if (target - candidates[i] < 0) { // 剪枝。当前i已经比target大了,那么i后面的肯定都要比target大 break; } path.addLast(candidates[i]); dfs(ans, path, candidates, i, target - candidates[i]); // 恢复现场,用于回溯 path.removeLast(); } } }
优化后的代码,时间复杂度和前面的是一样的,但是却能够提前过滤掉许多数据,同时降低了递归的次数,不仅有效降低了时间损耗,也降低了空间损耗
-
解法二:LeetCode官放提供的解法,回溯+剪枝
import java.util.ArrayList; import java.util.Deque; import java.util.LinkedList; import java.util.List; /** * @author ghp * @title 组合总和 */ class Solution { public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> ans = new ArrayList<>(10); Deque<Integer> path = new LinkedList<>(); dfs(ans, candidates, target, path, 0); return ans; } public void dfs(List<List<Integer>> ans, int[] candidates, int target, Deque<Integer> path, int step) { if (step == candidates.length) { // 遍历到最底层了还没有发现 return; } if (target == 0) { ans.add(new ArrayList<>(path)); return; } // 直接遍历下一层 dfs(ans, candidates, target, path, step + 1); // 遍历当前层 if (target - candidates[step] >= 0) { // 这个if相当于进行一个筛选(也就是剪枝) path.addLast(candidates[step]); dfs(ans, candidates, target - candidates[step], path, step); path.removeLast(); } } }
复杂度分析:
- 时间复杂度: O ( S ) O(S) O(S),S为所有可行解的长度之和
- 空间复杂度: O ( t a r g e t ) O(target) O(target)