文章目录
问题描述
给定一个整数数组 nums
,找出并返回所有不同的非递减子序列。子序列中至少有两个元素,且各元素保持相对顺序(非递减)。
示例:
- 输入:
nums = [4,6,6]
- 输出:
[[4,6], [4,6,6], [6,6]]
错误代码分析
原代码实现
java
class Solution {
public List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backTrack(result, new ArrayList(), nums, 0);
return result;
}
public void backTrack(List<List<Integer>> result, List<Integer> path, int[] nums, int index) {
if (path.size() > 1) {
result.add(new ArrayList(path));
}
for (int i = index; i < nums.length; i++) {
if (path.size() > 0 && nums[index] < path.get(path.size() - 1)) {
continue;
}
path.add(nums[i]);
backTrack(result, path, nums, i + 1);
path.remove(path.size() - 1);
}
}
}
错误原因
-
非递减条件错误
- 错误代码 :
nums[index] < path.getLast()
- 问题 :
index
是递归的起始位置,而实际应该比较的是当前元素nums[i]
与路径末尾元素。 - 后果:错误地跳过了有效的子序列。
- 错误代码 :
-
缺少去重逻辑
- 问题 :未处理同一层级的重复元素,导致生成重复子序列。例如输入
[4,6,6]
会生成两个[4,6]
。 - 后果:结果集中包含重复答案。
- 问题 :未处理同一层级的重复元素,导致生成重复子序列。例如输入
修正方案与代码实现
修正后的代码
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
public List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backTrack(result, new ArrayList<>(), nums, 0);
return result;
}
public void backTrack(List<List<Integer>> result, List<Integer> path, int[] nums, int index) {
if (path.size() > 1) {
result.add(new ArrayList<>(path));
}
Set<Integer> used = new HashSet<>(); // 关键:记录当前层已使用的元素
for (int i = index; i < nums.length; i++) {
// 1. 非递减检查:当前元素必须 >= 路径末尾元素
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)) {
continue;
}
// 2. 同一层级去重:跳过已使用的元素
if (used.contains(nums[i])) {
continue;
}
used.add(nums[i]);
path.add(nums[i]);
backTrack(result, path, nums, i + 1);
path.remove(path.size() - 1); // 回溯
}
}
}
关键修正点
-
非递减条件修正
- 将
nums[index]
改为nums[i]
,确保比较的是当前元素与路径末尾元素。
- 将
-
引入去重逻辑
- 使用
Set<Integer> used
记录当前层级已使用的元素,避免重复选择。例如[4,6₁,6₂]
中,同一层级的6₁
和6₂
会被去重。
- 使用
核心问题:为什么 used
不需要回溯?
作用域与生命周期
-
used
集合的生命周期:used
在每次进入新的递归层级时(即每次调用backTrack
方法时)被重新创建。- 当递归返回到父层级时,当前层级的
used
会被销毁,子层级的used
不影响父层级。
-
path
需要显式回溯的原因:path
是跨层级共享的。在递归过程中,子层级对path
的修改会直接影响父层级的状态,因此必须通过remove
操作撤销修改。
示例分析
以输入 nums = [4,6,6]
为例:
-
父层级(index=0):
- 选择
4
,进入子层级(index=1)。 - 子层级的
used
是独立的新集合。
- 选择
-
子层级(index=1):
used
初始化为空。- 处理
i=1
(元素6₁
):used
添加6₁
,path
变为[4,6₁]
。- 继续递归,收集有效子序列
[4,6₁]
。
- 回溯后,
path
恢复为[4]
。 - 处理
i=2
(元素6₂
):used
包含6₁
,因此跳过6₂
,避免重复。
总结
算法设计要点
- 非递减检查:确保子序列的每个新元素不小于路径末尾元素。
- 层级去重 :通过
used
集合避免同一层级选择重复元素。 - 递归与回溯 :显式维护
path
的状态,保证不同分支的独立性。
复杂度分析
- 时间复杂度:O(2^N * N),N 为数组长度。
- 空间复杂度:O(N),递归栈深度最大为 N。