LeetCode 46. 全排列

题目描述

给定一个不含重复数字的数组 nums,返回它的所有可能的全排列。

答案可以按任意顺序返回。

例如:

复制代码
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

初始思路

一开始可以把这道题归类为"每一步选哪个数"的问题。

因为数组中的数字不重复,所以只需要用一个布尔数组 onPath 记录每个下标对应的数字是否已经被选过。

递归过程中,每一层从 nums 中枚举一个还没有选过的数,把它加入当前路径 path,继续往下一层搜索。

解题思路

这题的关键是先明确 dfs(i) 的含义。

定义:

复制代码
dfs(i) 表示当前正在填排列中的第 i 个位置

当 i == nums.length 时,说明已经填完了一个完整排列,此时把 path 拷贝一份加入答案。

为什么要拷贝一份?

因为 path 在后续回溯过程中还会继续被修改。如果直接加入 path 本身,答案里的内容也会被后续修改影响。

每一层递归做的事情是:

复制代码
1. 枚举 nums 中的每个位置 j
2. 如果 nums[j] 没有在当前路径中,就选择它
3. 标记 onPath[j] = true,并把 nums[j] 加入 path
4. 递归填写下一个位置
5. 递归结束后撤销选择

也就是典型的回溯模板:

复制代码
做选择
递归
撤销选择

代码实现

复制代码
class Solution {
    List<List<Integer>> ans;

    public List<List<Integer>> permute(int[] nums) {
        ans = new ArrayList<>();
        int len = nums.length;
        boolean[] onPath = new boolean[len];
        List<Integer> path = new ArrayList<>();
        dfs(0, path, nums, onPath);
        return ans;
    }

    public void dfs(int i, List<Integer> path, int[] nums, boolean[] onPath) {
        if (i == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        for (int j = 0; j < nums.length; j++) {
            if (!onPath[j]) {
                path.add(nums[j]);
                onPath[j] = true;

                dfs(i + 1, path, nums, onPath);

                path.remove(path.size() - 1);
                onPath[j] = false;
            }
        }
    }
}

为什么这样写

全排列可以理解为"从左到右依次填位置"。

假设 nums = [1, 2, 3],递归树可以这样理解:

复制代码
第 0 个位置:可以选 1、2、3
第 1 个位置:只能从剩下没选过的数里选
第 2 个位置:继续从剩下没选过的数里选

每条从根节点走到叶子节点的路径,都是一个完整排列。

onPath[j] 的作用就是保证每个数字在同一个排列中只会被使用一次。

易错点

1. 没有说清楚 dfs 的含义

这题不能只模糊地理解成"递归选数"。

更准确的定义应该是:

复制代码
dfs(i):当前正在填第 i 个位置

有了这个定义,终止条件 i == nums.length 就很自然。

2. 忘记撤销选择

递归前做了两件事:

复制代码
path.add(nums[j]);
onPath[j] = true;

递归后就必须对应撤销:

复制代码
path.remove(path.size() - 1);
onPath[j] = false;

否则当前分支的选择会影响后面的分支。

3. 加入答案时没有拷贝 path

不能写成:

复制代码
ans.add(path);

因为 path 是同一个列表对象,后续还会继续修改。

正确写法是:

复制代码
ans.add(new ArrayList<>(path));

4. removeLast() 的兼容性

如果 path 声明为 List<Integer>,并且使用的是常见的 ArrayList 写法,更稳妥的是:

复制代码
path.remove(path.size() - 1);

不要依赖 removeLast(),避免在不同 Java 版本或平台环境下出现兼容问题。

复杂度分析

  • 时间复杂度:O(n * n!)。一共有 n! 个排列,每个排列拷贝到答案中需要 O(n)。
  • 空间复杂度:O(n)。递归栈、path 和 onPath 都需要 O(n);如果把返回结果也计入空间,则为 O(n * n!)。

复盘

这题的回溯框架并不复杂,核心问题是要把递归函数的含义定义清楚。

我一开始的方向是对的:用布尔数组记录哪些数已经选过,再用 DFS 枚举所有可能。

真正需要修正的是:不要只停留在"选哪个数"的感觉上,而要明确当前递归层正在填排列中的哪个位置。这样终止条件、选择逻辑和撤销逻辑都会更清楚。

Tips

全排列这类题可以记住一句话:

复制代码
每一层填一个位置,每个位置枚举所有还没选过的数。

只要递归前后能做到"选择"和"撤销选择"严格对应,代码就不容易乱。

相关推荐
武子康1 小时前
Qwen-Drive 到底是什么:4B 级 VLM 如何成为驾驶策略底座
人工智能·算法·机器学习
zh_xuan3 小时前
LeeCode 496. 下一个更大元素I
数据结构·算法·栈·单调栈
机 _ 长3 小时前
05 智算运维|8 张卡怎么才算「捆成一张」:NCCL 实测与拓扑
运维·人工智能·算法·智算运维
早起CaiCai4 小时前
【2023-01 OM】利用深度学习算法融合异质观测实现海洋场降尺度
人工智能·深度学习·算法
梦里寻码4 小时前
我用 NestJS + Vue3 写了一套完整的研发管理系统(58 表 / 229 API / 130 项断言,全开源
前端·后端·算法
猿人谷5 小时前
Surrogate Model:让进化算法学会“预测”,为什么它正在成为智能优化的重要技术?
后端·算法·架构
文丑颜不良啊5 小时前
LeetCode2241~2260题目解析
java·leetcode
文丑颜不良啊6 小时前
LeetCode2321~2340题目解析
java·leetcode
青少儿编程课堂6 小时前
蓝桥杯青少组图形化 STEMA 省考(报名倒计时·11/22开考)+ 全国青少年信息素养大赛 2026—2027赛季 + 电子学会图形化考级:校园华容道大闯关
c++·python·算法·bfs·信息学竞赛