全排列
回溯深度优先搜索排列约 1153 字大约 4 分钟
HJ150. 全排列
题目描述
给定整数 n,按字典序输出 1 到 n 的所有排列。
输入格式
一行一个整数 n。
输出格式
每行输出一个排列(n 个整数,空格分隔),按字典序升序。
示例
示例 1
输入:
3输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1说明:
1 到 3 的所有 6 种排列,按字典序升序输出。
解题思路
第一步:理解问题本质
生成 1 到 n 的所有排列,共 n! 种,并按字典序排列。本质是经典的全排列问题。
第二步:暴力解法(嵌套交换)
使用迭代式交换(如 Heap 算法)或逐个推导排列。
问题:此类方法生成顺序不保证字典序,且需要额外排序,逻辑复杂。
第三步:优化解法(回溯 / DFS)
用回溯法逐步构建排列:path 保存当前已选数字,used 记录哪些数字已用。每次从 1 到 n 中选一个未使用的数字填入当前位置,递归到长度为 n 时记录一个排列。
关键:从小到大遍历候选数字,天然保证生成的排列是字典序。
def backtrack():
# 已选够 n 个数,一个完整排列成型,收进结果后返回上一层
if len(path) == n:
res.append(' '.join(map(str, path)))
return
for x in range(1, n + 1): # 从小到大,保证字典序
# 已经用过的数字不能再放,否则一个排列里会出现重复
if not used[x]:
# 选它:标记占用,并追加到当前路径末尾
used[x] = True
path.append(x)
# 继续填下一位
backtrack()
# 撤销选择,把数字还回去,供同一层的下一个候选使用
path.pop()
used[x] = False第四步:最优解法
回溯法本身就是全排列问题的最优思路,时间复杂度 O(n·n!)(生成 n! 个排列,每个 O(n)),空间 O(n)(回溯栈 + used 数组)。
完整代码实现
import sys
def solve() -> None:
# 读入一行并转成整数,n 就是要排列的数字个数
n = int(sys.stdin.readline().strip())
# res 收集全部排列的字符串形式,path 是当前正在拼的那一个排列
res = []
path = []
# 下标 1..n 对应数字 1..n,多开一格,避免 used[n] 越界
used = [False] * (n + 1)
def backtrack():
# 路径长度达到 n,说明每位都填好了,转成字符串存档
if len(path) == n:
res.append(' '.join(map(str, path)))
return
for x in range(1, n + 1): # 从小到大,保证字典序
# 只考虑还没用过的数字,已用过的会让排列出现重复
if not used[x]:
# 做选择:占用 x 并把它放到当前这一位
used[x] = True
path.append(x)
# 递归填下一位
backtrack()
# 撤销选择,回到填这一位之前的状态
path.pop()
used[x] = False
# 从空排列开始搜索
backtrack()
# 攒够后一次性输出,比在递归里逐行 print 快得多
print('\n'.join(res))
if __name__ == "__main__":
solve()示例推演
以示例 1 为例,n=3。
回溯过程(每次从 1 开始选,保证字典序):
- 选 1 → 选 2 → 选 3,得到
1 2 3 - 回溯:退 3,选 3 → 得到
1 3 2 - 回溯:退到 1,退 2,选 2 → 选 1 → 选 3,得到
2 1 3 - 选 2 → 选 3 → 选 1,得到
2 3 1 - 选 3 → 选 1 → 选 2,得到
3 1 2 - 选 3 → 选 2 → 选 1,得到
3 2 1
完整字典序序列:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 交换法+排序 | O(n!·log n!) | O(n) | 需额外排序 |
| 回溯法 | O(n·n!) | O(n) | 生成 n! 个排列,天然字典序 |
易错点总结
1. 忘记回溯恢复状态
递归返回后必须 path.pop() 并 used[x] = False,否则状态被污染,导致排列缺失或错误。
2. 遍历顺序不保证字典序
若从大到小遍历候选,生成的是逆字典序。必须从 1 到 n 递增遍历。
3. used 数组大小
used 下标从 1 到 n,数组长度应为 n+1,写成 n 会越界。
扩展思考
- 若输入有重复元素,需要先排序并在回溯中跳过相同元素(
if i > 0 and nums[i]==nums[i-1] and not used[i-1]: continue)去重。 - 排列数增长极快(n!),n 较大时必须考虑剪枝或改用 next_permutation 迭代生成。