LeetCode 47. 全排列 II
题目描述

题意分析
返回数组元素能组成的所有不同排列,每条排列都必须使用全部输入元素且每个下标恰好使用一次。不同位置可以存放相同值,但交换两个相同值不会产生新排列,结果中只能保留一份。
排列区分元素顺序,答案列表本身可以按任意顺序返回。不能先把输入数组去重,因为每种数值的出现次数必须保留;需要同时处理“下标不能重复使用”和“最终数值序列不能重复”两个要求。
解法:排序 + used 去重回溯
核心思路
[!blue]
用
path保存已经填好的排列前缀,一层递归决定下一个位置放什么。因为排列允许任意顺序,每层都要从整个数组中选择,而不能只枚举后面的下标;used[i]表示下标i是否已经放入当前路径,保证同一元素不会被复用。仅靠
used仍会重复:相等值来自不同下标,交换它们的选择顺序会得到相同的数值序列。先排序使相等值相邻,再规定这些相等元素只能按下标从小到大的顺序进入路径。因此,当
nums[i] == nums[i - 1]且used[i - 1]为假时,不能选择i。前一个相等元素尚未进入当前前缀,先选它就能覆盖先选i所能生成的全部数值排列,当前分支没有必要重复搜索。若前一个相等元素已经在路径中,则可以继续使用当前元素:此时是在同一排列的后续位置放入另一个相同值,并不是重复生成整个排列。这就是条件必须包含
!used[i - 1]的原因。任意一个合法的数值排列,都能把其中同值元素依出现先后映射到排序后递增的下标,因此上述限制不会漏解;同值下标的次序又被唯一确定,所以不会重复。路径长度达到
n时复制保存,随后撤销本轮选择和标记,让其他分支从相同前缀继续搜索。
解题步骤
- 排序数组,使相同数字相邻。
- 用
used标记当前路径已使用的下标;每一层仍从0到n - 1枚举。- 跳过已使用下标;再用
i > 0 && nums[i] == nums[i - 1] && !used[i - 1]剪掉同层重复分支。- 选择当前数字、递归下一层,返回后撤销路径和
used状态。- 路径长度等于
n时复制一份加入答案。
代码实现
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
boolean[] used = new boolean[nums.length];
dfs(nums, used, new ArrayList<>(), res);
return res;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> res) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 前一个相等元素未在路径中时,当前选择属于同层重复。
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
path.add(nums[i]);
dfs(nums, used, path, res);
// 路径与使用标记一起撤销,让兄弟分支从相同状态出发。
path.remove(path.size() - 1);
used[i] = false;
}
}
}
import "sort"
func permuteUnique(nums []int) [][]int {
sort.Ints(nums)
res := make([][]int, 0)
path := make([]int, 0, len(nums))
used := make([]bool, len(nums))
var dfs func()
dfs = func() {
if len(path) == len(nums) {
perm := append([]int(nil), path...)
res = append(res, perm)
return
}
for i := 0; i < len(nums); i++ {
if used[i] {
continue
}
// 前一个相等元素未在路径中时,当前选择属于同层重复。
if i > 0 && nums[i] == nums[i-1] && !used[i-1] {
continue
}
used[i] = true
path = append(path, nums[i])
dfs()
// 路径与使用标记一起撤销,让兄弟分支从相同状态出发。
path = path[:len(path)-1]
used[i] = false
}
}
dfs()
return res
}
复杂度分析
- 时间复杂度:最坏为 $O(n \times n!)$。元素互不相同时共有 $n!$ 个结果,每个结果复制路径需要 $O(n)$;排序的 $O(n \log n)$ 被该项覆盖。
- 空间复杂度:$O(n)$,用于递归栈、路径和
used数组;不计返回结果。
关键点总结
[!green]
- 去重模板是“排序 + 同层跳过相邻重复值”;排列额外需要
used,组合/子集通常使用起点下标。!used[i - 1]的本质是给相同元素规定唯一使用顺序,而不是简单地“看到相同数字就跳过”。- 收集答案时必须复制路径;路径是所有递归层共享并反复修改的对象。
易错点总结
[!yellow]
- 不排序就比较相邻元素,无法识别被其他数值隔开的重复元素,会生成重复排列。
- 去重条件不能省略
!used[i - 1];若相邻相等就无条件跳过,同值元素中只有第一个能进入路径,无法用完全部输入元素。res.add(path)或 Go 的append(res, path)都没有复制底层数据,后续回溯会改坏已收集结果。- 递归返回后必须同时撤销路径和
used;只恢复其中一个都会污染兄弟分支。- 只在结果阶段用集合去重,仍会搜索所有等值下标互换形成的重复分支;应在选择下一项时直接剪掉这些分支。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 46. 全排列 | 中等 | 不含重复值时无需同层去重,本题排序后按相同元素的使用次序剪枝。 |
| 90. 子集 II | 中等 | 同样通过排序和选择顺序消除重复结果,但子集与排列对元素顺序的要求不同。 |
| 60. 排列序列 | 困难 | 逐位选择未使用元素构造排列;本题排序后增加同层重复剪枝,该题用阶乘块大小直接定位第 k 个排列。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!