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

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

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

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

相关推荐
zyl7656 小时前
FlashAttention 详解:从 GPU 硬件、普通 Attention 到分块计算与 Online Softmax
算法
爱读书的小胖10 小时前
LeetCode 122:买卖股票的最佳时机 II(C)解题分析
算法
结局无敌10 小时前
React Native for OpenHarmony 实战:三方库 react-native-modal-dialog 的鸿蒙化适配指南
深度学习·算法·react native·react.js·harmonyos
OPEN-F11 小时前
ROS2系列教程:上位机建图与导航(原理综合)
算法·机器人
weixin_3077791311 小时前
C++代码实现MATLAB中的fitrtree函数功能
开发语言·c++·算法·matlab
tomwang032211 小时前
路径优化建模、优化算法分类及工程化小结
算法·数学建模·分类
小羊没烦恼!11 小时前
Ajax与JSON的一些总结
android·java·前端·数据库·mysql·算法·okhttp
2501_9269783311 小时前
人机通信协议--人和机器之间的交互想象
人工智能·经验分享·算法·ai写作
行者全栈架构师11 小时前
【鸿蒙心迹】互动卡片全链路——把核心功能放到桌面(FormKit 实战)(HarmonyOS 7.x)
前端·算法·架构
无定义_11 小时前
10.11日常训练
算法