leetcode 46. 全排列 python3

本文介绍了如何使用回溯法解决全排列问题,给出了一种基于Python的DFS解决方案。代码详细展示了如何递归地生成一个数组的所有可能排列,并提供了示例输入和输出。此算法适用于长度不超过6且不包含重复数字的数组。

 时间:2022-3-21

题目地址:力扣

题目难度: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 中的所有整数 互不相同


思路1:回溯

代码段1:通过

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        def dfs(nums,size,depth,path,used,result):
            print(nums,size,depth,path,used,result)
            if size == depth:
                result.append(path[:])
                return
            for i in range(size):
                if not used[i]:
                    used[i] = True
                    path.append(nums[i])
                    dfs(nums,size,depth+1,path,used,result)
                    used[i] = False
                    path.pop()
                
        size = len(nums)
        used = [False for _ in range(size)]
        result = []
        dfs(nums,size,0,[],used,result)
        return result

总结:

  1. 后悔药之回溯 深度优先遍历

<think> 我们被要求回答LeetCode全排列问题的Python解决方案,并且参考了用户提供的站内引用信息。 从引用中,我们可以提取以下信息: 引用[2]提供了一个解决排列问题的回溯算法,但是该算法处理的是无重复元素的情况(因为它检查nums[i] not in path来避免重复使用元素)。同时,引用[4]提到了在处理字符串排列时的一个索引细节。 然而,用户提到了“全排列问题”,在LeetCode中有两道经典题目: 1. 46. 全排列(无重复数字) 2. 47. 全排列 II(可能有重复数字) 根据引用[2]中的代码,它实现的是无重复数字的全排列(使用深拷贝来保存结果,以及通过检查元素是否在path中来避免重复使用同一个元素)。但是,这种方法在判断元素是否使用过时,由于使用了`if nums[i] not in path`,当列表中有重复元素时,这种方法会失效,因为它无法区分两个相同的元素(即使它们在不同位置,但值相同,会被认为已经使用过而跳过)。因此,引用[2]的代码适用于无重复数字的情况。 对于有重复数字的情况,我们需要进行剪枝以避免重复的排列。 因此,我们将分别提供两个问题的解决方案: 问题1:无重复数字的全排列LeetCode 46) 问题2:有重复数字的全排列LeetCode 47) 另外,引用[4]提到的一个细节是:在回溯时,为了避免重复使用同一个元素,我们通常使用一个索引标记(start index)或者使用一个used数组记录哪些元素已经被使用。 在引用[2]的代码中,使用了`if nums[i] not in path`来避免使用同一个元素多次,但是这种方法在数组中有重复元素时是不正确的,因为重复元素会被视为同一个元素。所以对于无重复的情况,我们可以用这种方法,但更高效的方式是使用一个visited数组记录下标,这样即使有重复元素,我们也可以通过下标区分。 但是,由于LeetCode 46是无重复数字,所以我们可以使用下标记录法或者交换法。这里我们提供两种常用方法。 方法一:回溯(使用visited数组) 方法二:回溯(交换法,原地修改) 对于LeetCode 47(有重复数字),我们则需要更复杂的剪枝。 参考用户提供的引用,我们注意到引用[2]中提供了回溯框架,我们可以在此基础上改进。 首先,对于无重复数字的全排列46题): 我们可以使用一个visited数组来记录每个位置上的数字是否被使用过。 然后,对于有重复数字的全排列(47题): 我们需要在46题的基础上,先对数组排序,然后在回溯过程中判断当前数字是否和上一个数字相同,并且上一个数字是否已经被使用(或者未被使用)来进行剪枝。 下面我们分别给出代码: ### 46. 全排列(无重复) 方法一:使用visited数组 时间复杂度:O(n*n!) 空间复杂度:O(n) ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: def backtrack(path): if len(path) == len(nums): res.append(path[:]) # 注意这里用拷贝 return for i in range(len(nums)): if not visited[i]: visited[i] = True path.append(nums[i]) backtrack(path) path.pop() visited[i] = False res = [] visited = [False] * len(nums) backtrack([]) return res ``` 方法二:交换法(原地) 这种方法通过交换数组中的元素来实现,不需要额外空间记录访问状态,但是会修改原数组(通常题目允许,因为回溯会再交换回来)。 注意:这种方法得到的排列顺序可能和第一种不同,但都是全排列。 ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: def backtrack(first=0): if first == n: res.append(nums[:]) return for i in range(first, n): # 将当前元素与第一个元素交换 nums[first], nums[i] = nums[i], nums[first] backtrack(first+1) # 撤销交换 nums[first], nums[i] = nums[i], nums[first] n = len(nums) res = [] backtrack() return res ``` ### 47. 全排列 II(有重复数字) 对于有重复数字的情况,我们需要剪枝。这里使用visited数组方法,并配合排序和剪枝条件。 ```python class Solution: def permuteUnique(self, nums: List[int]) -> List[List[int]]: nums.sort() # 排序,使得相同数字相邻 res = [] visited = [False] * len(nums) def backtrack(path): if len(path) == len(nums): res.append(path[:]) return for i in range(len(nums)): if visited[i]: continue # 剪枝条件:当前元素和前一个元素相同,并且前一个元素没有被使用(说明已经回溯撤销了,此时应该跳过以避免重复) # 或者另一种写法:如果前一个元素被使用过,那么当前相同的元素还可以使用。这里我们使用:如果前一个相同元素没有被使用,那么当前也不能使用,否则会产生重复。 # 实际上,剪枝的关键是:避免在同一个位置多次填入相同的数字。 if i > 0 and nums[i] == nums[i-1] and not visited[i-1]: continue visited[i] = True path.append(nums[i]) backtrack(path) path.pop() visited[i] = False backtrack([]) return res ``` 注意:剪枝条件中的`not visited[i-1]`也可以替换成`visited[i-1]`?这里需要解释: - 如果`visited[i-1]`为True,表示前一个相同的元素已经被使用,那么当前元素可以继续使用(例如[1,1,2],第一个1被使用了,第二个1可以被使用)。 - 如果`visited[i-1]`为False,表示前一个相同的元素没有被使用,那么当前元素不能使用,因为会生成和之前重复的排列(例如,第一个1没有被使用,而第二个1被使用,那么相当于跳过了第一个1而使用第二个1,这样在后续排列中再用第一个1时,就会生成重复的排列)。 因此,我们使用`if i > 0 and nums[i] == nums[i-1] and not visited[i-1]:` 来跳过当前的这个重复元素。 另外,也可以使用`if i>0 and nums[i]==nums[i-1] and visited[i-1]:`,这样是不对的?实际上,两种剪枝条件都可以,但是含义不同: 1. 上面我们写的是`not visited[i-1]`:表示当遇到重复元素时,如果前一个重复元素没有被使用,那么当前元素就不应该被使用(因为在前一个元素没有被使用的情况下,使用当前元素会导致同一层级出现两个相同的元素分支,从而产生重复)。 2. 另一种剪枝条件:`if i>0 and nums[i]==nums[i-1] and visited[i-1]`,这种情况是跳过已经使用过的?这并不符合逻辑。 实际上,正确的剪枝有两种方式,但这里我们使用最常见的一种:`not visited[i-1]`。 但是,在某些情况下,使用`visited[i-1]`为真来剪枝也是可以的?其实不行。我们需要避免的是同一层级上的重复选择。具体可以参考以下解释: - 当我们在同一层级上,遇到两个相同的数字(比如当前数字和上一个数字相同),并且上一个数字没有被使用(即已经被回溯撤销了),那么如果我们再使用当前数字,就会造成重复(因为上一个相同的数字在后续中还会被使用,这样就会产生相同的排列)。所以我们要跳过。 - 如果上一个相同的数字已经被使用了(visited[i-1]为True),那么当前数字和上一个数字不在同一层级(它们分别在两个不同的递归层级上),所以不会产生重复。 因此,我们使用`not visited[i-1]`作为剪枝条件。 总结:我们提供了两个问题的解决方案,并解释了剪枝条件。 最后,用户可能还想了解其他方法,但回溯是解决全排列问题最直接的方法。 根据要求,生成相关问题: §§相关问题§§ 1. 全排列问题的时间复杂度是多少? 2. 如何用迭代(非递归)的方法实现全排列3. 全排列问题在LeetCode中的题目编号是多少? 4. 有重复数字的全排列问题如何剪枝? 5. 除了回溯法,还有哪些方法可以生成全排列? </think> ### LeetCode全排列问题的Python解决方案 全排列问题在LeetCode中主要有两个经典题目: 1. **46. 全排列**(无重复元素):[LeetCode链接](https://leetcode.com/problems/permutations/) 2. **47. 全排列 II**(含重复元素):[LeetCode链接](https://leetcode.com/problems/permutations-ii/) 以下是两种场景的Python解决方案,基于回溯算法实现: --- #### 1. 无重复元素的全排列LeetCode 46) ```python class Solution: def permute(self, nums: List[int]) -> List[List[int]]: def backtrack(path): # 终止条件:路径长度等于原始数组长度 if len(path) == len(nums): res.append(path[:]) # 深拷贝当前路径 return for num in nums: if num in path: # 避免重复使用元素 continue path.append(num) backtrack(path) # 递归探索 path.pop() # 回溯 res = [] backtrack([]) return res ``` **核心逻辑**: - 使用`path`记录当前排列 - 遍历所有数字,跳过已在`path`中的元素 - 递归完成后通过`path.pop()`回溯 - 时间复杂度:$O(n \times n!)$(共有$n!$种排列,每种需要$O(n)$时间复制) --- #### 2. 含重复元素的全排列LeetCode 47) ```python class Solution: def permuteUnique(self, nums: List[int]) -> List[List[int]]: def backtrack(path, used): if len(path) == len(nums): res.append(path[:]) return for i in range(len(nums)): # 跳过已使用或重复且未解锁的元素 if used[i] or (i > 0 and nums[i] == nums[i-1] and not used[i-1]): continue used[i] = True path.append(nums[i]) backtrack(path, used) path.pop() used[i] = False # 回溯 nums.sort() # 关键:排序使相同元素相邻 res = [] used = [False] * len(nums) backtrack([], used) return res ``` **核心改进**: 1. **排序预处理**:`nums.sort()`使相同元素相邻 2. **剪枝条件**: - `used[i]`跳过已用元素 - `nums[i] == nums[i-1] and not used[i-1]`跳过重复分支 3. **状态标记**:`used`数组精确追踪元素使用状态 - 时间复杂度:$O(n \times n!)$(最坏情况) --- ### 关键算法解析 1. **回溯框架**: - 选择 → 递归 → 撤销选择 - 通过`path.pop()`和状态重置实现回溯 2. **重复处理**: - 排序后通过相邻比较检测重复 - `not used[i-1]`确保同层级不重复使用相同值[^4],避免重复排列 3. **深拷贝必要性**: - `res.append(path[:])`防止后续操作修改结果 > **示例输入输出**: > - 输入 `[1,2,3]` → 输出 `[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]` > - 输入 `[1,1,2]` → 输出 `[[1,1,2],[1,2,1],[2,1,1]]` --- ### 相关问题 1. 为什么全排列问题的时间复杂度是$O(n \times n!)$? 2. 如何优化全排列算法以降低空间复杂度? 3. 回溯算法在处理排列组合问题时有哪些通用模板? 4. 当数组长度较大时(如 n>10),如何避免全排列计算的性能瓶颈? 5. 除了回溯法,还能用哪些算法解决全排列问题(例如Heap算法)? [^1]: LeetCode动态规划类问题(如零钱兑换)与排列问题解法差异显著 [^2]: 回溯法通过决策树遍历所有可能解,需注意状态重置 [^3]: 二维搜索问题(如搜索矩阵)使用双指针而非回溯 [^4]: 剪枝条件`not used[i-1]`确保同层级不重复使用相同值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值