本系列使用IDEA+LEETCODE EDITOR插件,题目描述统一英文。
题目链接
一、题目描述:
//Given two integers n and k, return all possible combinations of k numbers out
//of 1 ... n.
//
// You may return the answer in any order.
//
//
// Example 1:
//
//
//Input: n = 4, k = 2
//Output:
//[
// [2,4],
// [3,4],
// [2,3],
// [1,2],
// [1,3],
// [1,4],
//]
//
//
// Example 2:
//
//
//Input: n = 1, k = 1
//Output: [[1]]
//
//
//
// Constraints:
//
//
// 1 <= n <= 20
// 1 <= k <= n
二、思路分析:
全排列是比较常见的可以用回溯解决的问题。回溯是算法中模板比较固定的一种解法,通常使用递归。
流程就是选择一个加入临时结果集,传入并调用下次递归,然后从临时结果集中移除刚才添加的项。
此题无非多了不固定返回条数这一点而已。并没有什么难度,勉强算得上是M。
用递归解决问题的性能要点,如何剪枝,这个才是这期的重点内容。
三、AC 代码:
class Solution {
public List<List<Integer>> combine(int n, int k) {
// 0. 题目描述中已经对n,k做了限制,此处不再进行异常数据判断,只初始化结果
List<List<Integer>> result = new ArrayList<>();
// 1. 求全部组合,一般都可以用回溯算法解决,根据题意可用题目给的n代替数组
// 2. 调用回溯算法
// 存储中间结果使用
List<Integer> tmpList = new ArrayList<>();
backtrace(n,k,1,result,tmpList);
return result;
}
void backtrace(int n,int k,int currentIndex,List<List<Integer>> result,List<Integer> tmpList){
// 添加到返回结果的条件
if(tmpList.size() == k){
// 数组数字有序唯一,此处不需要判重
result.add(new ArrayList<>(tmpList));
return;
}
// 进行递归
for (int i = currentIndex; i <= n; i++) {
tmpList.add(i);
// 此处容易写错,因为i是变化的,要传入i的值而不是currentIndex
backtrace(n,k,i + 1,result,tmpList);
tmpList.remove(tmpList.size()-1);
}
}
}
此时并未进行剪枝,执行结果:
解答成功:
执行耗时:22 ms,击败了39.47% 的Java用户
内存消耗:39.9 MB,击败了37.60% 的Java用户
并不令人满意,现在思考如何剪枝。
思考一下场景:
当n=4,k=3,currentIndex=4,tmpList=[3]的时候,是否还需要进行递归?
并不需要,那么你判断的依据是什么? 要返回k=3个数,但是之前只添加了一个数,此时index已经指向了最后一个数,1+1=2,2<3,所以哪怕tmpList.size()<k,也不需要继续执行浪费资源了,因为最后得到结果肯定不是我们要的。
所以n-currentIndex + 1的值也就是剩余可循环的数量,要大于等于k-tmpList.size()还需加入的数据的数量。在循环内currentIndex变为i,所以得出类似初中数学的方程式:
n-i + 1>=k-tmpList.size()
在for循环中需要改写为i<=xxxx的形式,变换为如下:
i<= n- (k-tmpList.size()) + 1
归纳为代码则内容如下
for (int i = currentIndex; i <= n-(k - tmpList.size()) + 1; i++) {
tmpList.add(i);
// 此处容易写错,因为i是移动的,要传入i的值而不是currentIndex
backtrace(n,k,i + 1,result,tmpList);
tmpList.remove(tmpList.size()-1);
}
解答成功:
执行耗时:2 ms,击败了93.47% 的Java用户
内存消耗:39.7 MB,击败了74.64% 的Java用户
四、总结:
- 记住回溯模板,非常简单。记住口诀:剩余循环中-加-递归-删。
- 使用递归时切记要进行剪枝提高性能。
- 掌握基本的方程式变换用于算出生成循环的判断条件。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情