给定两个整数 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]]
常规的思路处理应该是使用for循环嵌套,达到目的。但是我们会发现,k值作为变量,我们不知道它的具体值,因此这种方式是不妥当的。而且随着for循环数量的增多,时间复杂度也会变得非常大,是不合适的。这时候就要采用递归回溯的方式求解问题了。为了更加直观得反应回溯中的递归调用,我们将回溯的过程抽象成树形结构,如下:
保存最终答案使用二维List数组,临时数组采用Deque保存,Deque满足后直接存入List里。
选取一个数字后在剩下的数再选取一个,直到达成最终的集合。 回溯的时间复杂度就是递归调用树深度。当临时数组长度满足条件后,跳出递归。跳出递归的条件也就是
//tmp是临时结果保存,使用Deque
//ans是结果数组,用List存储
if(tmp.size() == k){
ans.add(new linkedList<>(tmp));
return;
}
在递归调用树中我们可以看到,数字在每一层当中是横向遍历。因此 采用for循环进行这种横向调用。但是为了不重复使用相同数字,起始角标用start保存。
for(int i=start;i<=n;i++){
tmp.addLast(i);
backtracking(n,k,i+1);
tmp.removeLast();
}
添加完之后选择继续回溯递归,当结束递归后就将tmp刚添加的数字清楚,准备为下一次的添加做准备。整体代码如下:
class Solution {
List<List<Integer>> ans = new LinkedList<>();
Deque<Integer> tmp;
public List<List<Integer>> combine(int n, int k) {
tmp = new LinkedList<>();
backtracking(n,k,1);
return ans;
}
public void backtracking(int n,int k,int start){
if(tmp.size()==k){
ans.add(new LinkedList<>(tmp));
return;
}
for(int i=start;i<=n;i++){
tmp.addLast(i);
backtracking(n,k,i+1);
tmp.removeLast();
}
}
}
但是,这样的代码还不是最完美的,我们仍然可以使用剪枝法进行优化。
当n=4,k=4时,我们得到的回溯树图如下:
会发现,第一层2,3,4的值都没有必要去取。第二层3,4 第三层4没必要取。因此对于他们的循环就是一种浪费时间空间的事情。为了剪去这些无关数字,我们在for循环处想办法。为了减少之后数字出现,我们修改n为n-(k-tmp.size())+1 为什么+1?这样想,n=4,k=4,开始循环时tmp.size()==0。因此为了能取到1,需要+1;如果理解不了,画一个4取3个数的树图看下。
剪枝之后代码如下:
for(int i=start;i<=n-(k-tmp.size())+1;i++){
tmp.addLast(i);
backtracking(n,k,i+1);
tmp.removeLast();
}
最终时间空间复杂度均下降。