力扣刷题day25|491递增子序列、46全排列、47全排列 II

491. 递增子序列

力扣题目链接

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

思路

这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。但是这道题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。

所以不能使用之前的去重逻辑!

本题是在混乱的序列中(每一个元素的前后相对位置都固定了),找到有序递增的序列,比如在示例2中得不到递增序列[3, 4]

难点:去重

用[4, 7, 7]举例,在图中可以看出,同一父节点下的同层上使用过的元素就不能再使用了

image-20221018204810391

回溯三部曲
  1. 递归函数参数

求递增的子序列,肯定是不能重复使用数组中的元素的,所以要startIndex,调整下一层递归的起始位置。

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, int startIndex) {
  1. 终止条件

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

但本题收集结果有所不同,题目要求递增子序列大小至少为2

if (path.size() > 1) { // 序列至少有2个元素
    res.add(new ArrayList<>(path));
    // 注意这里不要加return,要取树上的节点
}
  1. 单层搜索逻辑

因为同一父节点下的同层上使用过的元素就不能在使用了,所以我们用set保存单层使用过的元素,i每取到一个元素的时候都在set里找是否存在,如果有就是之前使用过,直接continue跳过

// 单层
HashSet<Integer> set = new HashSet<>(); // set用来保存这一层出现过的元素
for (int i = startIndex; i < nums.length; i++) {
    // 如果当前的元素必path里的元素还要小,直接下一个i
    if (!path.isEmpty() && nums[i] < path.getLast()) {
        continue;
    }
    // 如果当前的元素在之前用过,直接下一个i
    if (set.contains(nums[i])) {
        continue;
    }
    set.add(nums[i]); // 不用回溯,仅用来记录用过的元素
    path.add(nums[i]);
    backtracking(nums, i + 1);
    path.removeLast();
}

递归函数上面的**set.add(nums[i]);**,下面却没有对应的pop之类的操作,因为set只负责本层,每到新的一层set都会重新定义(清空)

完整代码:

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

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, int startIndex) {
    if (path.size() > 1) { // 序列至少有2个元素
        res.add(new ArrayList<>(path));
        // 注意这里不要加return,要取树上的节点
    }
    if (startIndex == nums.length) {
        return;
    }

    // 单层
    HashSet<Integer> set = new HashSet<>(); // set用来保存这一层出现过的元素
    for (int i = startIndex; i < nums.length; i++) {
        // 如果当前的元素必path里的元素还要小,直接下一个i
        if (!path.isEmpty() && nums[i] < path.getLast()) {
            continue;
        }
        // 如果当前的元素在之前用过,直接下一个i
        if (set.contains(nums[i])) {
            continue;
        }
        set.add(nums[i]); // 不用回溯,仅用来记录用过的元素
        path.add(nums[i]);
        backtracking(nums, i + 1);
        path.removeLast();
    }
}
用数组优化

其实用数组来做哈希,效率就高了很多

注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。

并且在使用数组时,为了保证下标不溢出,下标要+100,用过的元素,在数组中赋值为1,碰到为1的元素i就跳到下一个

// 单层
int[] used = new int[201];; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]
for (int i = startIndex; i < nums.length; i++) {
    // 如果当前的元素必path里的元素还要小,直接下一个i
    if (!path.isEmpty() && nums[i] < path.getLast()) {
        continue;
    }
    // 如果当前的元素在之前用过,直接下一个i
    if (used[nums[i] + 100] == 1) {
        continue;
    }
    used[nums[i] + 100] = 1; // 不用回溯,仅用来记录用过的元素
    path.add(nums[i]);
    backtracking(nums, i + 1);
    path.removeLast();
}

46. 全排列

力扣题目链接

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

思路

首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方

所以在树的每个分支不能用上面的父节点,但是能用其余剩下的节点,不需要startIndex来限制只能从某个位置开始

但如下图,排列问题需要一个used数组,标记已经选择的元素:

回溯三部曲
  1. 递归函数参数

全局变量path和res存储路径和最后结果,不需要startIndex,但要布尔型数组来记录该元素是否用过

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
  1. 递归终止条件

到达叶子节点就是结束的的时候。

因为要求的是全排列,当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

if (path.size() == nums.length) {
    res.add(new ArrayList<>(path));
    return;
}
  1. 单层搜索的逻辑

注:每层都是从0开始搜索而不是startIndex

used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。uesd数组不是用来判断前一个,而是判断当前的是否为ture,为true说明这个元素在上层用过了,就不能再用了,直接continue跳过

// 单层
for (int i = 0; i < nums.length; i++) {
    if (used[i] == true) { // 如果当前的元素用过了,就跳过不用了
        continue;
    }
    used[i] = true;
    path.add(nums[i]);
    backtracking(nums, used);
    used[i] = false;
    path.removeLast();
}

完整代码:

public List<List<Integer>> permute(int[] nums) {
    boolean[] used = new boolean[nums.length];
    backtracking(nums, used);
    return res;
}

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));
        return;
    }

    // 单层
    for (int i = 0; i < nums.length; i++) {
        if (used[i] == true) { // 如果当前的元素用过了,就跳过不用了
            continue;
        }
        used[i] = true;
        path.add(nums[i]);
        backtracking(nums, used);
        used[i] = false;
        path.removeLast();
    }
}

47. 全排列 II

力扣题目链接

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

思路

这道题是给定一个可包含重复数字的序列,要返回所有不重复的全排列

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

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

image-20221019123128008

回溯三部曲
  1. 递归函数参数

全局变量path和res存储路径和最后结果,不需要startIndex,但要布尔型数组来记录该元素是否用过

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
  1. 递归终止条件

到达叶子节点就是结束的的时候。

因为要求的是全排列,当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

if (path.size() == nums.length) {
    res.add(new ArrayList<>(path));
    return;
}
  1. 单层搜索的逻辑

判断去重的逻辑时,除了判断nums[i] == nums[i - 1],还要判断i-1位的used的真假

注:如果不加used[i - 1]的判断,那就会在树枝、树层都去重,就多删除了满足条件的排列

  • 树层中前一位去重

如果要对树层中前一位去重,就用used[i - 1] == false

对树层的去重很彻底,在上层的时候就可以排除其他情况,且是在不重复的普通全排列基础上修改,更容易理解

  • 树枝前一位去重

如果要对树层中前一位去重,就用used[i - 1] == true

数值的去重效率不高,需要往下遍历很深

image-20221019123141876

对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!

树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。

完整代码:

public List<List<Integer>> permuteUnique(int[] nums) {
    Arrays.sort(nums);
    boolean[] used = new boolean[nums.length];
    backtracking(nums, used);
    return res;
}

LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
    if (path.size() == nums.length) {
        res.add(new ArrayList<>(path));
        return;
    }

    // 单层
    for (int i = 0; i < nums.length; i++) {
        if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
            continue;
        }
        if (used[i] == true) {
            continue;
        }
        used[i] = true;
        path.add(nums[i]);
        backtracking(nums, used);
        used[i] = false;
        path.removeLast();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值