一、题目描述
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 9
1 <= n <= 60
二、解题思路
这是一个典型的组合问题,我们可以使用回溯法(Backtracking)来解决。回溯法是一种试探性的算法,通过尝试各种可能的组合来找到所有符合条件的解。
以下是解题步骤:
- 从数字1开始,尝试所有可能的数字组合。
- 对于每一个数字,都有两种选择:选择这个数字或不选择这个数字。
- 如果选择了这个数字,就从剩余的数字中选择下一个数字,直到选择了k个数字为止。
- 在每一步选择之后,检查当前组合的和是否等于n。
- 如果当前组合的和等于n,并且已经选择了k个数字,那么这个组合就是一个有效的解,将其添加到结果列表中。
- 如果当前组合的和大于n,或者已经没有足够的数字可以选择,那么这个组合不是一个有效的解,回溯到上一步。
- 由于每个数字只能使用一次,我们需要保证在递归的过程中不会重复选择同一个数字。
三、具体代码
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> result = new ArrayList<>();
backtrack(result, new ArrayList<>(), k, n, 1);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> tempList, int k, int remain, int start) {
if (tempList.size() == k && remain == 0) {
// 找到一个有效的组合
result.add(new ArrayList<>(tempList));
return;
}
for (int i = start; i <= 9; i++) {
if (remain - i < 0) break; // 如果当前数字大于剩余的和,则不需要继续尝试
tempList.add(i); // 选择当前数字
backtrack(result, tempList, k, remain - i, i + 1); // 递归,注意i+1,保证每个数字只使用一次
tempList.remove(tempList.size() - 1); // 回溯,撤销选择
}
}
public static void main(String[] args) {
Solution solution = new Solution();
List<List<Integer>> combinations = solution.combinationSum3(3, 7);
for (List<Integer> combination : combinations) {
System.out.println(combination);
}
}
}
这段代码中,backtrack
函数是核心的回溯函数,它尝试从start
开始的每一个数字,检查它们是否可以组成和为remain
的k个数字的组合。如果找到一个有效的组合,就将其添加到结果列表中。在每次递归调用之后,都会撤销最后的选择,即从临时列表中移除最后一个数字,以便尝试其他可能的组合。
四、时间复杂度和空间复杂度
1. 时间复杂度
-
回溯函数
backtrack
对于每一个数字都有两种选择:选择或不选择。这意味着对于每一个数字,函数都会被递归调用两次(除了递归的边界条件)。 -
因为组合的长度为k,所以我们最多只能选择k个数字。
-
在最坏的情况下,我们需要遍历所有的数字组合,直到找到所有符合条件的组合。
-
假设数字范围是1到9,那么在递归过程中,我们最多会有9个选择点,每个选择点都有选择和不选择两种情况,所以递归的深度最多是k,宽度是9。
-
因此,递归的时间复杂度是O(2^k),但由于我们每次递归都会检查当前组合的长度是否为k,并且当组合长度为k时递归会停止,所以实际的时间复杂度会小于O(2^k)。
-
在每个递归步骤中,我们还会进行一次循环,这个循环最多会执行9次(当start为1时)。
-
综合考虑,时间复杂度可以表示为O(9^k),因为每次递归都会进行一次长度为9的循环。
2. 空间复杂度
-
空间复杂度主要取决于递归的深度和递归过程中存储临时组合的列表。
-
递归的深度是k,因此递归栈的空间复杂度是O(k)。
-
在递归过程中,我们使用了一个临时列表
tempList
来存储当前的数字组合。在最坏的情况下,这个列表的大小也是k。 -
因此,空间复杂度主要取决于递归栈的深度和临时列表的大小,总的空间复杂度是O(k)。
五、总结知识点
-
回溯算法:
backtrack
方法:实现了回溯算法,用于找到所有可能的组合。- 递归:
backtrack
方法内部调用自身,实现递归。 - 回溯:在递归的每一步,都会尝试一种可能的选择,并在回溯时撤销这个选择。
-
数据结构:
List<List<Integer>>
:用于存储最终结果的列表,这是一个列表的列表。ArrayList
:Java集合框架中的类,用于实现动态数组。
-
循环和条件判断:
for
循环:用于遍历从start
到9的所有数字。if
语句:用于检查是否找到了一个有效的组合(tempList.size() == k && remain == 0
),以及当前数字是否可以加入组合(remain - i < 0
)。
-
列表操作:
add
方法:向列表中添加一个元素。remove
方法:从列表中移除最后一个元素,用于回溯操作。
-
方法调用:
new ArrayList<>(tempList)
:创建一个新的ArrayList
实例,并将其初始化为tempList
的副本,用于存储找到的有效组合。
-
递归终止条件:
- 当组合的长度达到k且和等于n时,递归终止,并将当前组合添加到结果列表中。
-
参数传递:
int k, int n
:方法参数,分别表示组合中数字的数量和组合的目标和。List<List<Integer>> result, List<Integer> tempList
:回溯方法中的参数,分别表示结果列表和当前组合的临时列表。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。