这个题目不是太难,主要记录一下自己从183ms击败1.3%提交的代码优化到3ms击败100%提交的代码的这个过程。希望能从中可以获取优化代码的思路。
77. Combinations
Given two integers n and k, return all possible combinations of k numbers out of 1 … n.
Example:
input: n = 4, k = 2
Output:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
原题地址:https://leetcode.com/problems/combinations/description/
在做leetCode题目77. Combinations 时,通过逐步的优化,将运行时间由180+ms缩短到3ms,从击败1.3%提交的代码,优化到击败100%。现在对这个过程进行一个总结:首先完成基本的功能,通过测试用例。
通过所有测试用例的代码如下:
class Solution {
public static List<List<Integer>> combine(int n, int k) {
if(n<1||k<1||k>n){
//注意
return null;
}
int[] nums = new int[n];
Arrays.setAll(nums, i -> i+1);
return computeCombine(nums, k, new LinkedList<>());
}
public static List<List<Integer>> computeCombine(int[] nums, int k, LinkedList<Integer> stack){
List<List<Integer>> result_list = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
if(nums[i] > -1){
stack.push(nums[i]);
nums[i] = -1;
if(k-1 == 0){//不需要再添加元素了
List<Integer> Arr_list = new ArrayList<>();
Arr_list.addAll(stack);
result_list.add(Arr_list);
}else {
int[] nums2 = Arrays.copyOf(nums, nums.length);
result_list.addAll(computeCombine(nums2, k-1, stack));
}
stack.pop();
}
}
return result_list;
}
}
上面代码的运行时间149ms,击败1.27%提交的代码。
然后对代码的时间复杂度分析,发现时间长的原因是搜索的范围太大,还有就是有Set集合的复制,有很多的存在性判断。第一次优化,针对上面的问题,改进了算法,缩小的搜索范围,不再需要存在性判断。提交的代码击败了37%的提交代码。
第一次优化后的代码如下:
class Solution {
public static List<List<Integer>> combine(int n, int k) {
if(n<1||k<1||k>n){
//注意
return null;
}
List<List<Integer>> result_list = new ArrayList<>();
computeCombine(n, k, 1, new LinkedList<>(), result_list);
return result_list;
}
public static void computeCombine(int n, int k, int start, LinkedList<Integer> stack, List<List<Integer>> result_list){
for (int i = start; i <=n ; i++) {
stack.push(i);
if(k==1){
result_list.add(new ArrayList<>(stack));
}else {//递归调用
computeCombine(n, k-1, i+1, stack, result_list);
}
stack.pop();
}
}
}
第一次优化后,代码通过所有测试用例的时间为37ms,击败37.84%提交的代码。优化力度很大,缩小了搜索范围,并且不再需要元素是不是已经使用过的存在性判断。
第二次优化,然后发现,还是有很多的搜索是无用的,这些搜索在一开始就注定不能搜索到合适的结果。通过剪枝策略,将这部分一开始就注定没有用的搜索分支过滤掉。提交代码后发现通过测试用例的时间缩短为3ms,击败了100%提交的代码。还是比较开心的,所以把这个过程记录下来,希望可以从中感受优化代码运行效率的过程。
第二次优化后的代码如下:
class Solution {
public static List<List<Integer>> combine(int n, int k) {
if(n<1||k<1||k>n){
//注意
return null;
}
List<List<Integer>> result_list = new ArrayList<>();
computeCombine(n, k, 1, new LinkedList<>(), result_list);
return result_list;
}
public static void computeCombine(int n, int k, int start, LinkedList<Integer> stack, List<List<Integer>> result_list){
//对for循环的结束条件进行了优化,有"i <= n"变为" i <= (n-k+1)",过滤掉所有没有结果的搜索。
for (int i = start; i <=(n-k+1) ; i++) { //注意: 在这儿for的结束条件变为i <= (n-k+1)
stack.push(i);
if(k==1){
result_list.add(new ArrayList<>(stack));
}else {//递归调用
computeCombine(n, k-1, i+1, stack, result_list);
}
stack.pop();
}
}
}
第二次优化后,代码通过所有测试用例的时间为3ms,击败了100%提交的代码。也算是自己优化了一早上没有白费功夫。主要做的优化,就是将for循环的结束条件由” i <= n”改变为” i <= (n - k +1)”,可以过滤掉所有不能产生结果的搜索,这样就很大的提升了算法搜索效率。
对这次整个的优化过程进行一个提炼,也方便以后参考。
优化过程: 提炼: 分析时间复杂度-> 找到最耗时操作->改进算法->再分析时间复杂度-> 消除冗余运算。