💥LeetCode 47. 全排列 II 超详细代码解析
在算法的世界里,全排列问题是经典的回溯算法应用场景。今天我们来深入剖析 LeetCode 上第 47 题 —— 全排列 II,带你轻松拿捏这道题!😎
题目描述
给定一个可包含重复数字的序列nums ,按任意顺序返回所有不重复的全排列。
代码实现
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
backtrack(nums, 0, new ArrayList<>(), res);
return res;
}
private void backtrack(int[] nums, int mark, List<Integer> curr, List<List<Integer>> res) {
if (curr.size() >= nums.length) {
res.add(new ArrayList<>(curr));
return;
}
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i-1] && ((mark >> (i - 1)) & 1) == 0 || ((mark >> i) & 1) == 1) {
continue;
}
curr.add(nums[i]);
backtrack(nums, mark | (1 << i), curr, res);
curr.remove(curr.size() - 1);
}
}
}
代码思路分析
1. 初始化和排序
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
backtrack(nums, 0, new ArrayList<>(), res);
return res;
- 首先创建一个res列表,用于存储所有的全排列结果。
- 然后对nums数组进行排序,这一步非常关键!排序是为了后续方便处理重复元素,避免生成重复的排列。这一步的时间复杂度为 ,其中n是nums数组的长度,因为使用了类似快速排序的算法进行排序。
- 调用backtrack回溯函数,从索引 0 开始,当前排列为空列表new ArrayList<>(),结果列表为res。这一步只涉及简单的变量赋值和函数调用,时间复杂度为 。空间复杂度方面,除了输入数组和结果列表,额外使用的空间主要是几个变量,所以这部分空间复杂度为 。
2. 回溯函数backtrack
private void backtrack(int[] nums, int mark, List<Integer> curr, List<List<Integer>> res) {
if (curr.size() >= nums.length) {
res.add(new ArrayList<>(curr));
return;
}
-
回溯函数的参数:
-
nums是输入的数组。
-
mark是一个标记位,用于记录哪些元素已经被使用过。
-
curr是当前正在生成的排列。
-
res是存储所有排列结果的列表。
-
-
当curr的大小大于等于nums的长度时,说明已经生成了一个完整的排列,将其添加到res中并返回。添加操作的时间复杂度为 ,因为要复制当前排列列表,空间复杂度也为 ,因为新创建了一个列表来存储当前排列。
3. 遍历和剪枝
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i-1] && ((mark >> (i - 1)) & 1) == 0 || ((mark >> i) & 1) == 1) {
continue;
}
curr.add(nums[i]);
backtrack(nums, mark | (1 << i), curr, res);
curr.remove(curr.size() - 1);
}
-
遍历
nums数组中的每个元素:-
剪枝条件:
-
i > 0 && nums[i] == nums[i - 1] && ((mark >> (i - 1)) & 1) == 0这部分表示如果当前元素和前一个元素相同,并且前一个元素还没有被使用过,就跳过当前元素。这是为了避免生成重复的排列。 -
((mark >> i) & 1) == 1表示如果当前元素已经被使用过,也跳过。这两个条件判断的时间复杂度为 。
-
-
如果不满足剪枝条件,将当前元素添加到
curr中,时间复杂度为 。 -
更新
mark,将当前元素标记为已使用,即mark | (1 << i),时间复杂度为 。 -
递归调用
backtrack继续生成下一个排列。由于每个元素在每个位置都可能被放置(除了剪枝的情况),所以总的时间复杂度为 ,其中n是数组长度,n!是全排列的数量,前面的n表示每次递归调用中循环的次数。空间复杂度方面,递归调用栈的深度最大为n,再加上存储当前排列的列表,所以空间复杂度为 。 -
回溯,将当前元素从
curr中移除,以便尝试其他元素,时间复杂度为 。
总体时间复杂度:主要由排序和回溯过程决定,排序为 ,回溯为 ,所以总体时间复杂度为 ,因为 的增长速度远大于 。总体空间复杂度:除了输入数组和结果列表外,主要是递归调用栈和存储当前排列的空间,所以空间复杂度为 。
通过这样的回溯和剪枝操作,我们就能高效地生成所有不重复的全排列啦!🎉
希望这篇文章能帮助你更好地理解全排列 II 的解题思路和代码实现,大家一起加油,攻克更多算法难题!💪