全排列

标签: 数组 回溯

难度: Medium

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

Submission

运行时间: 32 ms

内存: 15.1 MB

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        result = []

        def backtrack(path):
            if len(path) == len(nums):
                result.append(list(path))
                return

            for i in range(len(nums)):
                if nums[i] is None:
                    continue
                path.append(nums[i])
                nums[i] = None
                backtrack(path)
                nums[i] = path.pop()

        backtrack([])
        return result

Explain

这个题解使用了回溯算法。首先定义了一个 backtrack 函数,它接收一个 path 参数表示当前的排列路径。如果 path 的长度等于 nums 的长度,说明找到了一个完整的排列,将其加入结果列表中并返回。否则,遍历 nums 中的每个元素,如果该元素还没有被使用过(即不为 None),就将其加入到 path 中,并将该元素在 nums 中标记为 None 表示已使用。然后递归调用 backtrack 函数,继续搜索下一个位置的元素。当递归返回时,将之前标记为 None 的元素恢复为原来的值,并从 path 中移除,以便尝试其他的排列方式。最后,在主函数中调用 backtrack 函数,并传入一个空的 path,开始搜索所有可能的排列。

时间复杂度: O(n * n!)

空间复杂度: O(n * n!)

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        result = []

        def backtrack(path):
            # 如果 path 的长度等于 nums 的长度,说明找到了一个完整的排列
            if len(path) == len(nums):
                result.append(list(path))
                return

            for i in range(len(nums)):
                # 如果当前元素已经被使用过,跳过
                if nums[i] is None:
                    continue
                # 将当前元素加入到 path 中
                path.append(nums[i])
                # 将当前元素在 nums 中标记为已使用
                nums[i] = None
                # 递归调用,继续搜索下一个位置的元素
                backtrack(path)
                # 恢复当前元素在 nums 中的值
                nums[i] = path.pop()

        backtrack([])
        return result

Explore

在回溯算法中,将已选元素标记为 None 而不是从 nums 数组中删除,是为了避免在删除和插入操作中产生的额外时间开销。使用标记的方法可以直接通过修改数组元素的值来表示这个元素已被使用,这样可以保持数组的结构不变,使得回溯到上一个状态时,只需简单地将标记撤销即可。这种方法比删除元素后再将其插回原位置要高效得多,因为数组的插入和删除操作的时间复杂度为 O(n),而直接修改元素的时间复杂度为 O(1)。

每次递归调用 backtrack 之后,需要恢复 nums[i] 的原始值并从 path 中移除最后一个元素,是因为这样可以撤销当前递归步骤中的选择,从而保证回溯到上一个状态时,环境与当时的选择前一致。这一操作是回溯算法的核心,即“试错”。如果不将元素恢复原状,那么在返回上一层递归时,上一层的循环将无法正确地继续尝试其他可能的元素,因此这种“状态重置”是实现正确的搜索和回溯的关键。

如果 nums 数组中包含重复元素,现有的算法可能无法正确处理,因为简单的标记 None 可能导致无法区分不同位置但值相同的元素。为了处理包含重复元素的情况,可以先将 nums 排序,然后修改回溯算法,使用一个额外的布尔数组来跟踪元素的使用状态,而非直接修改 nums 元素为 None。此外,在递归之前检查当前元素是否与前一个元素相同,并且前一个元素的使用状态为未使用,如果是,则跳过当前元素。这样可以防止生成重复的排列。

在回溯算法中使用 list(path) 而非直接使用 path 添加到结果集中,是因为 path 在整个回溯过程中是一个被不断修改的变量。如果直接将 path 添加到结果集,那么存储在结果集中的将是 path 的引用,而非其当时的值。随着回溯过程中 path 的修改,结果集中的内容也会跟随变化。使用 list(path) 是为了创建 path 的一个浅拷贝,这样即使后续 path 发生变化,已经添加到结果集中的排列也不会改变,确保存储了每一个独立的排列快照。