代码随想录算法训练营第29天 | 491.递增子序列 ,46.全排列 ,47.全排列 II

回溯章节理论基础:

https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html

491.递增子序列

题目链接:https://leetcode.cn/problems/non-decreasing-subsequences/

思路:

本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!

本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。

为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:

在这里插入图片描述
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。

本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题!一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。

但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以需要加个判断。

同时,同一父节点下的同层上使用过的元素就不能再使用了。所以单层中,我们使用了一个hashset,每次都add一下。但是后面,没有对应的remove操作!

这里是需要注意的地方,hashset是记录本层元素是否重复使用,新的一层set都会重新定义(清空),所以要知道set只负责本层!

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> paths = new ArrayList<>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        backtracking(nums,0);
        return result;
    }

    public void backtracking(int[] nums, int startIndex){
        if(paths.size() >= 2)
            result.add(new ArrayList<>(paths));
        HashSet<Integer>set = new HashSet<>();
        for(int i=startIndex; i < nums.length ; i++){
            if(paths.size() >0 && paths.get(paths.size() - 1) > nums[i]  || set.contains(nums[i])){
                continue;
            }
            paths.add(nums[i]);
            set.add(nums[i]);
            backtracking(nums, i+1);
            paths.removeLast();
        }
    }
}

时间复杂度: O(n * 2^n)
空间复杂度: O(n)

46.全排列

题目链接:https://leetcode.cn/problems/permutations/

思路:

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

此时我们已经学习了77.组合问题、 131.分割回文串 和78.子集问题 ,接下来看一看排列问题。

我以[1,2,3]为例,抽象成树形结构如下:

在这里插入图片描述
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。

可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。

但排列问题需要一个used数组,标记已经选择的元素,如上图的橘黄色部分所示。

终止情况:当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> paths = new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
       used = new boolean[nums.length];
       Arrays.fill(used,false);
       backTracking(nums);
       return result;
    }

    public void backTracking(int[] nums){
        if(paths.size() == nums.length)
        {
            result.add(new ArrayList<>(paths));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i]) continue;   // 如果使用过,就跳过
            used[i] = true;
            paths.add(nums[i]);
            backTracking(nums);
            paths.removeLast();
            used[i] = false;
        }
    }
}

时间复杂度: O(n!)
空间复杂度: O(n)

47.全排列 II

题目链接:https://leetcode.cn/problems/permutations-ii/

思路:

这道题目和46.全排列 的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列。

这里又涉及到去重了。去重的套路和40.组合总和II 、90.子集II 里的组合问题和子集问题的去重套路一样。

同时,去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。

我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:

在这里插入图片描述
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。

在46.全排列 中已经详细讲解了排列问题的写法,在40.组合总和II 、90.子集II 中详细讲解了去重的写法,所以代码也就很好写出来了。

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> paths = new ArrayList<>();
    boolean[] used2;
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        used2 = new boolean[nums.length];
        Arrays.fill(used2,false);
        backtracking(nums,used2);
        return result;
    }

    public void backtracking(int[] nums, boolean[] used){
        if(paths.size() == nums.length)
        {
            result.add(new ArrayList<>(paths));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(used[i]) continue;   // 如果使用过,就跳过
            // 如果在同一树层使用过,就跳过
            if(i>0 && nums[i] == nums[i-1] && used[i-1] == false){
                continue;
            }
            used[i] = true;
            paths.add(nums[i]);
            backtracking(nums,used);
            paths.removeLast();
            used[i] = false;
            
        }

    }
}

时间复杂度: O(n! * n)
空间复杂度: O(n)

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值