Java回溯算法解决非递减子序列问题(LeetCode 491)的深度解析
文章目录
- 问题描述
- 错误代码分析
- 原代码实现
- 错误原因
- 修正方案与代码实现
- 修正后的代码
- 关键修正点
- 核心问题:为什么 `used` 不需要回溯?
- 作用域与生命周期
- 示例分析
- 总结
- 算法设计要点
- 复杂度分析
问题描述
给定一个整数数组 nums
,找出并返回所有不同的非递减子序列。子序列中至少有两个元素,且各元素保持相对顺序(非递减)。
示例:
- 输入:
nums = [4,6,6]
- 输出:
[[4,6], [4,6,6], [6,6]]
错误代码分析
原代码实现
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]
。 - 后果:结果集中包含重复答案。
- 问题:未处理同一层级的重复元素,导致生成重复子序列。例如输入
修正方案与代码实现
修正后的代码
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。