目录
【491.递增子序列】中等题
思路:
1、处理当前节点
- 如果到当前节点的路径长度为1或者为0,直接遍历访问子节点即可
- 如果到当前节点的路径长度大于/等于2,则判断是否递增
- 如果递增,则记录路径
- 如果不是递增,则不记录路径,不访问子节点,直接返回
2、遍历子节点
- 在for循环遍历前,定义Set对象,用于记录当前层遍历过的子节点(注意:不能定义为全局变量,因为递归的时候会加入其它层的节点)。
- 在for循环遍历时,如果当前层前面出现过相同值的子节点,就不遍历该子节点,跳过。
难点:需要【判断子序列是否递增】和【考虑如何去重】
相似题目:【90.子集II】,但90题可以排序,通过与前一个子节点比较即可去重,而491题的结果与数组的元素顺序有关,不能排序,否则结果必错,所以需要使用额外的空间记录访问过的子节点。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backtracking(nums, 0);
return res;
}
public void backtracking(int[] nums, int start){
// 如果到当前节点的路径长度大于/等于2,则判断是否递增(路径长度为1或者为0,直接遍历访问子节点即可)
if (path.size() >= 2){
// 如果递增,则记录路径
if (path.get(path.size() - 1) >= path.get(path.size() - 2)) res.add(new ArrayList(path));
// 如果不是递增,则不记录路径,不访问子节点,直接返回
else return;
}
// 用于记录当前层遍历过的子节点(注意:不能定义为全局变量,因为递归的时候会加入其它层的节点)
Set<Integer> set = new HashSet<>();
for (int i = start; i < nums.length; i++){
// 如果当前层前面出现过,就不遍历该子节点,跳过
if (!set.isEmpty() && set.contains(nums[i])) continue;
set.add(nums[i]);
path.add(nums[i]);
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
【46.全排列】中等题
思路:
在遍历子节点的时候,先判断路径中是否已经包含想遍历的子节点,如果包含就不再遍历该子节点。
反思:
一开始自己实现的时候,使用了额外的Set对象记录访问过的节点,但是其实没有必要,因为额外Set对象做的事情和路径path变量做的事情一样,直接用path变量判断即可。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backtracking(nums);
return res;
}
public void backtracking(int[] nums){
// 终止条件(如果路径长度和数组长度一样,证明已经排列完毕,将路径记录到res中)
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
// 遍历子节点
for (int i = 0; i < nums.length; i++){
// 如果路径中已经遍历过这个节点,就不再遍历
if (path.contains(nums[i])) continue;
path.add(nums[i]);
backtracking(nums);
path.remove(path.size() - 1);
}
}
}
【47.全排列 II】中等题
思路:和【46.全排列】的区别在于,数组中的元素是可以重复的。
- 考虑树的纵向递归:要保证每个重复的元素都能用上,需要使用used数组记录元素的使用情况,而不能用简单的contains(存在重复元素,直接使用contains不合理)。
- 考虑树的横向遍历:如果当前子节点前面遍历过,则得跳过当前子节点,因此需要用额外的Set对象记录当前层遍历过的子节点。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length]; // 默认初始化值为false
backtracking(nums, used);
return res;
}
public void backtracking(int[] nums, boolean[] used){
// 长度一样则完成排列,记录结果并返回
if (path.size() == nums.length){
res.add(new ArrayList(path));
return;
}
Set<Integer> set = new HashSet<>(); // 用于记录当前层遍历过的子节点
for(int i = 0; i < nums.length; i++){
if (used[i] == true) continue; // 如果上层已经用过了该元素,则跳过
// 这里没有排序后直接和上一个元素比较,是因为上一个元素可能不是同一层的子节点
if (set.contains(nums[i])) continue;
set.add(nums[i]); // 记录当前层遍历过的子节点
used[i] = true;
path.add(nums[i]);
backtracking(nums, used);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
优化:不使用额外的空间记录当前层遍历过的子节点
- 问题:如果直接将nums先排序,再在递归for循环的时候,直接判断当前子节点是否与上一个子节点相同,这时无法保证上一个节点是当前层遍历过的子节点还是上层遍历过的节点。
- 方案:需要在判断时,确保上个位置的元素是当前层的子节点,才能跳过。如果当前子节点和上个位置元素的值相同,且上个位置的元素未出现在路径中(即上个位置的元素也是当前层已遍历过的子节点),则跳过。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length]; // 默认初始化值为false
backtracking(nums, used);
return res;
}
public 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; // 如果上层已经用过了该元素,则跳过
// 如果和上个位置元素的值相同,且上个位置的元素未出现在路径中(即上个位置的元素也是当前层已遍历过的子节点),则跳过
if (i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue;
used[i] = true;
path.add(nums[i]);
backtracking(nums, used);
used[i] = false;
path.remove(path.size() - 1);
}
}
}
总结:更加建议只使用used数组,而不用Set对象。
- 原因1:不需要使用额外的空间
- 原因2:不排序的去重有时候不一定完全能去重,存在风险,例如:例子。