1. 全排列问题概述
LeetCode第46题"Permutations"是算法学习中的经典问题,要求给定一个不含重复数字的数组,返回其所有可能的全排列。这个问题在技术面试中出现频率极高,据不完全统计,在亚马逊、谷歌等大厂的面试题库中出现率超过60%。
我第一次遇到这个问题是在准备Facebook面试时,当时花了整整3小时才写出正确的回溯解法。后来发现,这个看似简单的问题实际上包含了递归、回溯、深度优先搜索(DFS)等多个算法核心概念,是理解算法思维的最佳切入点之一。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与解法思路
2.1 问题描述与示例
给定一个不含重复数字的数组nums,返回所有可能的排列组合。例如:
- 输入:[1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这个问题与组合问题(Combinations)的关键区别在于:排列考虑顺序,而组合不考虑。这意味着[1,2]和[2,1]在排列中视为不同的结果。
2.2 解法思路比较
常见的解法主要有三种:
- 回溯法(递归实现)
- 迭代法(使用堆栈)
- 库函数法(使用语言内置排列函数)
回溯法是最直观且教学意义最大的解法,时间复杂度为O(n*n!),空间复杂度为O(n)。虽然看起来效率不高,但对于n≤10的输入规模完全足够。
提示:在实际面试中,面试官通常期望看到回溯解法,因为它能展示候选人对递归和算法思维的理解深度。
3. 回溯解法详细实现
3.1 基础回溯实现
python复制def permute(nums):
def backtrack(first=0):
if first == n:
output.append(nums[:])
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)
output = []
backtrack()
return output
这个实现的核心思想是通过交换元素来生成不同的排列。first参数表示当前处理的位置,当first == n时表示已经生成了一个完整排列。
3.2 使用visited数组的替代实现
另一种常见写法是使用visited数组来标记已使用的元素:
python复制def permute(nums):
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
这种写法虽然空间复杂度略高,但逻辑更加直观,适合初学者理解。
4. 算法优化与变种
4.1 剪枝优化
当输入数组包含重复元素时(如LeetCode 47题),需要进行剪枝优化。可以通过排序和跳过重复元素来实现:
python复制def permuteUnique(nums):
nums.sort()
res = []
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
backtrack([], [False]*len(nums))
return res
4.2 迭代解法
对于不喜欢递归的开发者,可以使用迭代方式实现:
python复制def permute(nums):
stack = [(nums, [])]
res = []
while stack:
nums, path = stack.pop()
if not nums:
res.append(path)
for i in range(len(nums)):
new_nums = nums[:i] + nums[i+1:]
stack.append((new_nums, path + [nums[i]]))
return res
这种解法使用显式栈来模拟递归过程,虽然代码稍长,但避免了递归的栈溢出风险。
5. 常见错误与调试技巧
5.1 新手常见错误
- 直接添加nums而非拷贝:错误写法
output.append(nums)会导致所有结果相同 - 忘记撤销交换:在回溯后必须恢复数组原始状态
- 终止条件错误:应该比较
first与数组长度而非当前路径长度
5.2 调试技巧
我常用的调试方法是在关键位置添加打印语句:
python复制def backtrack(first=0):
print(f"当前first={first}, nums={nums}, path={nums[:first]}")
if first == n:
output.append(nums[:])
print(f"找到排列: {nums[:]}")
return
for i in range(first, n):
nums[first], nums[i] = nums[i], nums[first]
print(f"交换 {first}和{i}: {nums}")
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first]
print(f"撤销交换: {nums}")
6. 实际应用场景
全排列算法不仅是面试题,在实际开发中也有广泛应用:
- 密码破解:生成所有可能的密码组合
- 游戏开发:棋盘游戏中的走法生成
- 测试用例生成:需要测试不同输入顺序的场景
- 数据分析:特征排列组合对模型的影响
我在实际项目中曾用排列算法解决过一个电商平台的优惠券组合问题,需要计算用户持有的多张优惠券所有可能的使用顺序对最终价格的影响。
7. 扩展学习建议
-
相关LeetCode题目:
-
- Permutations II(含重复元素)
-
- Next Permutation(下一个排列)
-
- Permutation Sequence(排列序列)
-
-
算法可视化工具:
- VisuAlgo的排列算法演示
- LeetCode官方解题动画
-
进阶学习:
- Heap's Algorithm(更高效的排列生成算法)
- 排列的字典序生成方法
- 排列与组合数学理论
掌握排列问题的关键在于理解递归树的结构和回溯的思想。建议初学者在白板上手动画出n=3时的递归调用树,这对理解算法执行流程非常有帮助。
