力扣题目链接
给你一个整数数组 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]举例,在图中可以看出,同一父节点下的同层上使用过的元素就不能再使用了
求递增的子序列,肯定是不能重复使用数组中的元素的,所以要startIndex,调整下一层递归的起始位置。
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, int startIndex) {
本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
但本题收集结果有所不同,题目要求递增子序列大小至少为2
if (path.size() > 1) { // 序列至少有2个元素
res.add(new ArrayList<>(path));
// 注意这里不要加return,要取树上的节点
}
因为同一父节点下的同层上使用过的元素就不能在使用了,所以我们用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();
}
力扣题目链接
给定一个不含重复数字的数组 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数组,标记已经选择的元素:
全局变量path和res存储路径和最后结果,不需要startIndex,但要布尔型数组来记录该元素是否用过
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
到达叶子节点就是结束的的时候。
因为要求的是全排列,当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
注:每层都是从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();
}
}
力扣题目链接
给定一个可包含重复数字的序列 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]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图:
全局变量path和res存储路径和最后结果,不需要startIndex,但要布尔型数组来记录该元素是否用过
LinkedList<Integer> path = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
void backtracking(int[] nums, boolean[] used) {
到达叶子节点就是结束的的时候。
因为要求的是全排列,当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
判断去重的逻辑时,除了判断nums[i] == nums[i - 1]
,还要判断i-1
位的used的真假
注:如果不加used[i - 1]
的判断,那就会在树枝、树层都去重,就多删除了满足条件的排列
如果要对树层中前一位去重,就用used[i - 1] == false
,
对树层的去重很彻底,在上层的时候就可以排除其他情况,且是在不重复的普通全排列基础上修改,更容易理解
如果要对树层中前一位去重,就用used[i - 1] == true
,
数值的去重效率不高,需要往下遍历很深
对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
完整代码:
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();
}
}