一、题目描述
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
二、解题思路
要解决这个问题,我们可以使用回溯算法。回溯算法是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化丢弃该解,即回溯并且再次尝试。
为了生成所有可能的组合,我们可以从数字1开始,依次尝试每一个数字,对于每一个数字,我们都可以选择将其放入当前的组合中,或者不放入。如果放入,我们就继续向组合中添加下一个数字;如果不放入,我们就尝试下一个数字。当我们组合中的数字数量达到k时,我们就找到了一个有效的组合,将其添加到结果列表中。
具体步骤如下:
1. 初始化一个结果列表result
,用于存储所有有效的组合。
2. 定义一个回溯函数backtrack
,该函数接受以下参数:
result
:结果列表comb
:当前组合start
:当前尝试的数字n
:数字范围上限k
:组合大小
3. 在backtrack
函数中,首先检查当前组合的大小是否等于k:
- 如果是,说明找到了一个有效的组合,将其添加到结果列表中,并返回。
- 如果不是,继续执行下一步。
4. 从start
开始,遍历所有可能的数字:
- 将当前数字添加到当前组合中。
- 递归调用
backtrack
函数,尝试下一个数字,即start + 1
。 - 在递归调用之后,将最后一个数字从当前组合中移除,以便尝试下一个数字。
5. 在combine
函数中,首先检查k是否小于等于0或n是否小于k:
- 如果是,返回空的结果列表。
- 如果不是,从1开始调用
backtrack
函数。
这样,我们就可以生成所有的组合,并将它们存储在结果列表中。最后,返回结果列表即可。
三、具体代码
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
if (k <= 0 || n < k) {
return result;
}
// 从1开始进行回溯
backtrack(result, new ArrayList<>(), 1, n, k);
return result;
}
private void backtrack(List<List<Integer>> result, List<Integer> comb, int start, int n, int k) {
if (comb.size() == k) {
// 找到一个有效的组合
result.add(new ArrayList<>(comb));
return;
}
for (int i = start; i <= n; i++) {
// 添加当前数字到组合中
comb.add(i);
// 递归地继续向组合中添加数字
backtrack(result, comb, i + 1, n, k);
// 回溯:移除最后一个数字,尝试下一个数字
comb.remove(comb.size() - 1);
}
}
}
四、时间复杂度和空间复杂度
1. 时间复杂度
- 对于每个数字,我们都有两种选择:包含在组合中或不包含。
- 对于n个数字,共有2^n种可能的组合,但题目要求我们只考虑大小为k的组合。
- 因此,我们不会探索所有2^n种可能性,而是根据组合数公式C(n, k)进行操作。
- 组合数公式C(n, k)表示从n个不同元素中取出k个元素的组合数,计算公式为C(n, k) = n! / (k! * (n-k)!).
- 因此,时间复杂度是O(C(n, k)),即O(n! / (k! * (n-k)!))。
2. 空间复杂度
- 空间复杂度主要取决于递归栈的深度和用于存储组合的列表。
- 递归栈的深度在最坏情况下是k,即当我们要找到一个大小为k的组合时,递归调用会进行k次。
- 每个组合需要一个大小为k的列表来存储,因此空间复杂度是O(k)。
- 综上所述,总的空间复杂度是O(k)。
五、总结知识点
-
回溯算法:这是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化丢弃该解,即回溯并且再次尝试。
-
递归:
backtrack
函数是一个递归函数,它通过自己调用自己来进行深度优先搜索(DFS)。递归允许函数在执行过程中调用自己,从而可以遍历所有可能的组合。 -
列表(ArrayList):
result
和comb
都是ArrayList
类型的列表,用于存储组合结果和当前的组合。ArrayList
是 Java 中的一个可调整大小的数组实现,提供了对元素的快速随机访问。 -
条件语句:代码中使用了条件语句(
if
)来检查组合的大小是否达到了k,以及输入参数是否有效。 -
循环语句:
for
循环用于遍历所有可能的数字,从start
到n
。 -
列表操作:
add
和remove
方法用于在组合列表中添加和移除元素。这是对列表进行动态操作的基本方法。 -
函数定义和调用:代码中定义了
combine
和backtrack
两个函数,combine
是公共函数,用于对外提供接口,而backtrack
是一个私有辅助函数,用于实现回溯逻辑。 -
参数传递:函数通过值传递(
int n, int k
)和引用传递(List<List<Integer>> result, List<Integer> comb
)来传递参数。在 Java 中,基本数据类型(如int
)是通过值传递的,而对象(如List
)是通过引用传递的。 -
深拷贝和浅拷贝:在将
comb
添加到result
时,使用了new ArrayList<>(comb)
来创建comb
的深拷贝。这是因为ArrayList
是可变的,如果不进行深拷贝,result
中存储的将是comb
引用的副本,而不是其值的副本。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。