77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
思路:回溯(有递归算法就有回溯算法,回溯搜索法也可叫做递归)
看到题目的描述,可用for循环遍历求出集合。比如 n = 4 k = 2
那么定义两个for循环遍历即可,for循环的次数是由k的大小决定
但k的值并不固定,那么for循环次数不确定,也就无法写代码
那么用另一种暴力算法。回溯
可将集合分解为树,并递归。那么如何分解呢?
比如 n = 4 k = 2。
回溯法也类似递归算法有固定的三要素
1.方法的入参和返回值 回溯法的入参不好确认,可以边写代码便确认。这题入参好确认 int n int k 返回值void
2.终止条件 path.size == 2
3.核心逻辑
定义for循环,循环的次数就是子树的数量。二叉树只有左右两节点,所以只递归两次
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
回溯操作,撤销本次操作结果
代码如下
//时间复杂度:O(C(n, k) * k),枚举结果总数为C(n, k),每次得到一个结果需要O(k)时间。
//空间复杂度:O(n),最大是n层递归栈。
static List<Integer> path = new ArrayList<>();
static List<List<Integer>> result = new ArrayList<>();
public static void main(String[] args) {
int n = 4;
int k = 2;
combine(4,2);
}
public static List<List<Integer>> combine(int n, int k) {
backTracking(n, k, 1);
return result;
}
public static void backTracking(int n, int k, int startIndex) {
if (path.size() == k) {// 中止条件
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n; i++) {
path.add(i);
backTracking(n, k, i + 1);
path.remove(path.size() - 1);
}
}
问题
给result结果赋值,忽略引用传递的问题。由于path的值在不断变化,那么会导致存在result中的值也在改动,最终导致result值不对
if (path.size() == k) {// 中止条件
result.add(path);
return;
}
改正后
if (path.size() == k) {// 中止条件
result.add(new ArrayList<>(path));
return;
}
思路:回溯 剪枝优化
回溯法虽然属于暴力算法的一种,但是也可以减枝优化,减少递归的次数
那么什么情况需要减枝呢?举例如下
n=k=4,那么就只有一种情况[1,2,3,4]能符合条件,其余的遍历,那都是浪费时间
https://code-thinking-1253855093.file.myqcloud.com/pics/20210130194335207-20230310134409532.png
如何避免?
减枝的位置,就在本层for循环处,我们可以限制i的上限
如果i后面的元素不足以满足k个元素,那么就不让for循环去搜索
当前已经选择的元素数量: path.size
还需要的元素数量: k-path.size
如果当前一个元素都没有选择,path.size = 0,k = n = 4
那么第一层循环i上限为1 = n-(k-path.size)+1,path.size = 0
第二层循环i上限为2= n-(k-path.size)+1,path.size = 1
第三层循环i上限为3= n-(k-path.size)+1,path.size = 2
第四层循环i上限为4= n-(k-path.size)+1,path.size = 3
为什么n-(k-path.size)+1 要加一?
因为如果i<=n-(k-path.size),在一开始k = n = 4,path.size = 0的时候,就无法加入第一个结点了
需要一个起始区间,所以更正为i<=n-(k-path.size)+1
代码如下
//时间复杂度:O(C(n, k) * k),枚举结果总数为C(n, k),每次得到一个结果需要O(k)时间。
//空间复杂度:O(n),最大是n层递归栈。
static List<Integer> path = new ArrayList<>();
static List<List<Integer>> result = new ArrayList<>();
public static List<List<Integer>> combine(int n, int k) {
backTracking(n, k, 1);
return result;
}
public static void backTracking(int n, int k, int startIndex) {
if (path.size() == k) {// 中止条件
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - k +path.size()+1; i++) {
path.add(i);
backTracking(n, k, i + 1);
path.remove(path.size() - 1);
}
}