LeetCode 第47题:全排列 II
题目描述
给定一个可包含重复数字的序列 nums ,按任意顺序返回所有不重复的全排列。
难度
中等
题目链接
示例
示例 1:
输入:nums = [1,1,2] 输出:[[1,1,2], [1,2,1], [2,1,1]]
示例 2:
输入:nums = [1,2,2] 输出:[[1,2,2], [2,1,2], [2,2,1]]
提示
1 <= nums.length <= 8-10 <= nums[i] <= 10
解题思路
回溯算法 + 剪枝
这道题是"全排列"的进阶版本,需要在原有回溯算法的基础上增加对重复元素的处理。关键是要理解如何避免生成重复的排列。
关键点:
- 首先对数组进行排序,使相同的数字相邻
- 使用标记数组记录每个位置的数字是否被使用
- 对于重复的数字,只有在前一个数字已经被使用的情况下才能使用当前数字
- 需要在回溯时正确恢复状态
具体步骤:
- 对输入数组进行排序
- 初始化结果列表和标记数组
- 定义回溯函数,增加重复元素的判断
- 遍历数组时跳过不合法的选择
- 递归生成所有合法排列
图解思路
算法步骤分析表
| 步骤 | 当前排列 | 可选数字 | 操作 | 说明 |
|---|---|---|---|---|
| 初始 | [] | [1,1,2] | 排序 | 数组已排序 |
| 选择1 | [1] | [1,2] | 选择第一个1 | 第一个数字 |
| 选择2 | [1,1] | [2] | 选择第二个1 | 允许使用重复数字 |
| 完成 | [1,1,2] | [] | 记录 | 得到一个排列 |
| 回溯 | [1] | [1,2] | 跳过重复 | 避免重复排列 |
状态/情况分析表
| 情况 | 输入 | 输出 | 说明 |
|---|---|---|---|
| 无重复 | [1,2,3] | 6种排列 | 同全排列I |
| 两数重复 | [1,1,2] | 3种排列 | 需要去重 |
| 全部重复 | [1,1,1] | 1种排列 | 只有一种可能 |
代码实现
C# 实现
public class Solution {
private IList<IList<int>> result = new List<IList<int>>();
private bool[] used;
public IList<IList<int>> PermuteUnique(int[] nums) {
Array.Sort(nums); // 先排序,使相同的数字相邻
used = new bool[nums.Length];
Backtrack(nums, new List<int>());
return result;
}
private void Backtrack(int[] nums, List<int> current) {
if (current.Count == nums.Length) {
result.Add(new List<int>(current));
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;
current.Add(nums[i]);
Backtrack(nums, current);
current.RemoveAt(current.Count - 1);
used[i] = false;
}
}
}
执行结果
- 执行用时:128 ms
- 内存消耗:42.8 MB
代码亮点
- 🎯 通过排序和剪枝高效去除重复排列
- 💡 巧妙利用used数组判断重复元素的使用情况
- 🔍 剪枝条件的设计既保证正确性又提高效率
- 🎨 代码结构清晰,易于理解和维护
常见错误分析
- 🚫 忘记对数组进行排序
- 🚫 剪枝条件写错,导致漏掉合法排列
- 🚫 重复元素的处理逻辑错误
- 🚫 回溯时状态恢复不完整
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 普通回溯 | O(n!×n) | O(n) | 简单直观 | 有重复结果 |
| 排序+剪枝 | O(n!×n) | O(n) | 无重复结果 | 需要排序 |
| HashSet去重 | O(n!×n) | O(n!×n) | 实现简单 | 空间消耗大 |