LC78 同样是回溯也有优劣之分的全部子集问题|刷题打卡

164 阅读1分钟

本系列使用IDEA+LEETCODE EDITOR插件,题目描述统一英文题目链接
一、题目描述:

//Given an integer array nums of unique elements, return all possible subsets (t
//he power set). 
//
// The solution set must not contain duplicate subsets. Return the solution in a
//ny order. 
//
// 
// Example 1: 
//
// 
//Input: nums = [1,2,3]
//Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
// 
//
// Example 2: 
//
// 
//Input: nums = [0]
//Output: [[],[0]]
// 
//
// 
// Constraints: 
//
// 
// 1 <= nums.length <= 10 
// -10 <= nums[i] <= 10 
// All the numbers of nums are unique. 

二、思路分析:
首先读题,看到求子集,首先想到的就是回溯算法。但是如果贸然去写会写出不是那么优秀的算法。 首先看劣等思路:
排序参数(题目只是给的例子是有序的,人家可没说给你的参数一定是有序的)。 每次回溯从参数下标开始往临时结果添加当前数字,因为每次都是从参数下标开始循环到数组结尾,会出现后面的重复添加的情况,以及先存了大数后存了小数导致最后的结果多余的情况。
需要在添加到结果集时增加判断。
代码可以AC但是性能很差:

		解答成功:
   		执行耗时:2 ms,击败了11.32% 的Java用户
   		内存消耗:38.7 MB,击败了60.93% 的Java用户

思考为什么性能这么差,发现劣等写法有排序,有循环。那么
1. 为什么每次回溯都是一个循环?
为了每次可以添加index到nums.length的多个可能结果,减少递归次数。
2. 一定要排序么?
因为一次回溯想要添加多个结果才需要的排序。

所以一次就加一个数字,递归多几层应该就能提高性能。注意优化版和劣质版添加返回结果的时机不同。优化版是在每次index已经等于数组长度的时候添加到返回。

如何找到劣质代码的优化点,这个才是这期的重点内容

三、AC 代码:

劣质版AC代码

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        // 很关键,题目描述没有说是有序的只是写了例子是有序的迷惑你,这里不判断回溯里会出问题的
        Arrays.sort(nums);
        List<List<Integer>> result = new ArrayList<>();
        backtrace(result,new ArrayList<>(),nums,0);
        result.add(new ArrayList<Integer>());
        return result;
    }
    void  backtrace(List<List<Integer>> result,List<Integer> tmp,int[] nums,int index){
        if (index >= nums.length) {
            return;
        };
        for (int i = index; i < nums.length; i++) {
            // 关键点,不是求全排列,是有顺序的
            if( tmp.size()==0 || (!tmp.contains(nums[i]) && tmp.get(tmp.size()-1) < nums[i])){
                tmp.add(nums[i]);
                result.add(new ArrayList<>(tmp));
                backtrace(result,tmp,nums,index + 1);
                tmp.remove(tmp.size() - 1);
            }
        }
    }
 }
	解答成功:
			执行耗时:2 ms,击败了11.32% 的Java用户
			内存消耗:38.9 MB,击败了10.80% 的Java用户

优化后AC代码,耗时已经打败了100%:

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrace(result, new ArrayList<>(), nums, 0);
        return result;
    }

    void backtrace(List<List<Integer>> result, List<Integer> tmp, int[] nums, int index) {
        // 临时结果集的数字个数跟当前层次匹配,则加入返回结果
        if (index == nums.length) {
            result.add(new ArrayList<>(tmp));
            return;
        }
        // 不选当前数
        backtrace(result, tmp, nums, index + 1);
        // 选了当前数
        tmp.add(nums[index]);
        backtrace(result, tmp, nums, index + 1);
        tmp.remove(tmp.size() - 1);
    }
}
		解答成功:
			执行耗时:0 ms,击败了100.00% 的Java用户
			内存消耗:38.7 MB,击败了58.42% 的Java用户

四、总结:

  1. 排序,重复计算是性能问题的常见点。
  2. 当发现代码量上去了且逻辑判断的次数多了时,就要思考代码是否有问题了。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情