💥LeetCode 47. 全排列 II 超详细代码解析

169 阅读4分钟

💥LeetCode 47. 全排列 II 超详细代码解析

在算法的世界里,全排列问题是经典的回溯算法应用场景。今天我们来深入剖析 LeetCode 上第 47 题 —— 全排列 II,带你轻松拿捏这道题!😎

题目描述

47. 全排列 II - 力扣(LeetCode)

给定一个可包含重复数字的序列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表示如果当前元素已经被使用过,也跳过。这两个条件判断的时间复杂度为 O(1)O(1)

  • 如果不满足剪枝条件,将当前元素添加到curr中,时间复杂度为 O(1)O(1)

  • 更新mark,将当前元素标记为已使用,即mark | (1 << i),时间复杂度为 O(1)O(1)

  • 递归调用backtrack继续生成下一个排列。由于每个元素在每个位置都可能被放置(除了剪枝的情况),所以总的时间复杂度为 O(n×n!)O(n \times n!) ,其中n是数组长度,n!是全排列的数量,前面的n表示每次递归调用中循环的次数。空间复杂度方面,递归调用栈的深度最大为n,再加上存储当前排列的列表,所以空间复杂度为 O(n)O(n)

  • 回溯,将当前元素从curr中移除,以便尝试其他元素,时间复杂度为 O(1)O(1)

总体时间复杂度:主要由排序和回溯过程决定,排序为 O(nlogn)O(nlogn),回溯为 O(n×n!)O(n \times n!),所以总体时间复杂度为 O(n×n!)O(n \times n!),因为 n×n!n \times n! 的增长速度远大于 nlognnlogn。总体空间复杂度:除了输入数组和结果列表外,主要是递归调用栈和存储当前排列的空间,所以空间复杂度为 O(n)O(n)

通过这样的回溯和剪枝操作,我们就能高效地生成所有不重复的全排列啦!🎉

希望这篇文章能帮助你更好地理解全排列 II 的解题思路和代码实现,大家一起加油,攻克更多算法难题!💪