LeetCode 46. 全排列:深度解析+代码拆解

LeetCode 经典回溯题------46. 全排列,这道题是回溯算法的入门必刷题,核心考察"穷举所有可能"的思路,虽然代码不长,但每一步都藏着回溯的精髓,新手很容易在"回溯回退"这一步踩坑,今天就带着大家逐行拆解,把思路讲透。

一、题目解读:什么是全排列?

题目很简单:给定一个不含重复数字的数组 nums,返回它所有可能的全排列,顺序不限制。

举个例子:如果 nums = [1,2,3],它的全排列就是所有不同顺序的组合,一共 3! = 6 种,分别是:

\[1,2,3\], \[1,3,2\], \[2,1,3\], \[2,3,1\], \[3,1,2\], \[3,2,1\]

关键注意点:数组不含重复元素,所以不需要考虑去重(这是和"全排列II"的核心区别),只需专注于"如何穷举所有顺序"。

二、核心思路:回溯法------"试错+回退"的穷举艺术

全排列的本质是"从剩余元素中不断选择一个,直到选完所有元素",这个过程就像走迷宫:

  1. 选一个元素放进"临时结果"(temp);

  2. 从剩下的元素中,再选一个放进临时结果;

  3. 重复步骤,直到没有剩余元素(此时临时结果就是一个完整的排列);

  4. "回退"一步,把最后选的元素拿出来,换另一个元素尝试(这就是"回溯"的核心)。

这种思路可以用「深度优先搜索(DFS)」来实现,DFS负责"深入选元素",回溯负责"回退换元素",两者结合就能穷举所有可能。

三、代码逐行拆解(附完整代码)

先贴出完整AC代码,再逐行拆解每一部分的作用,新手跟着走,一定能看懂:

typescript 复制代码
function permute(nums: number[]): number[][] {
    const res: number[][] = []; // 存储最终所有全排列结果

    // 递归函数:arr是剩余待选元素,temp是当前临时排列
    const dfs = (arr: number[], temp: number[]) => {
        // 终止条件:剩余元素为空,说明temp是一个完整排列
        if (arr.length === 0) {
            res.push([...temp]); // 深拷贝,避免后续修改影响结果
            return;
        }

        // 遍历剩余所有元素,逐个尝试选择
        for (let i = 0; i < arr.length; i++) {
            // 1. 从剩余元素中选出当前元素(排除第i个元素,得到新的剩余数组)
            const newArr = arr.filter((_, index) => index !== i);
            // 2. 将当前元素加入临时排列
            temp.push(arr[i]);
            // 3. 递归:继续从新的剩余元素中选择
            dfs(newArr, temp);
            // 4. 回溯:把刚才加入的元素拿出来,换下一个元素尝试
            temp.pop();
        }
    }

    // 初始调用:剩余元素是nums,临时排列为空
    dfs(nums, []);
    return res;
};

1. 变量初始化:res 存储最终结果

const res: number[][] = []; ------ 用来保存所有完整的全排列,比如上面例子中的6种组合,最终都会存在这里。

2. 核心递归函数 dfs:负责"选元素+回溯"

dfs 有两个参数:

  • arr:当前剩余待选择的元素(比如第一次调用时是nums,选了1之后,arr就变成[2,3]);

  • temp:当前正在构建的临时排列(比如选了1之后,temp就是[1],再选2就是[1,2])。

3. 终止条件:arr.length === 0

当剩余元素为空时,说明temp已经包含了所有nums的元素,是一个完整的排列,此时需要把temp加入res。

注意:这里必须用 [...temp] 深拷贝,而不是直接 res.push(temp)!因为temp是引用类型,后续回溯时会修改temp的值,如果直接push,res里的元素会跟着变,最后全是空数组。

4. 循环遍历:逐个尝试剩余元素

for (let i = 0; i < arr.length; i++) ------ 遍历当前所有剩余元素,每个元素都要尝试作为"下一个选中的元素"。

这部分是核心,拆解为4步:

  1. const newArr = arr.filter((_, index) => index !== i); ------ 生成新的剩余元素数组,排除当前选中的第i个元素(比如arr是[1,2,3],i=0时,newArr就是[2,3]);

  2. temp.push(arr[i]); ------ 把当前选中的元素加入临时排列(比如选中1,temp就变成[1]);

  3. dfs(newArr, temp); ------ 递归调用,继续从newArr中选元素,构建临时排列;

  4. temp.pop(); ------ 回溯的关键!把刚才加入的元素"拿出来",恢复temp的状态,方便下一次循环尝试其他元素(比如选了1之后,递归结束,pop掉1,temp变回空,再尝试选2)。

5. 初始调用:启动DFS

dfs(nums, []); ------ 第一次调用时,剩余元素是原始数组nums,临时排列为空,正式开始穷举。

四、关键易错点(新手必看)

  1. 深拷贝问题:res.push([...temp]) 不能写成 res.push(temp),否则会因为引用类型导致结果错误;

  2. 回溯回退:temp.pop() 必须放在递归调用之后,确保递归结束后,temp能恢复到上一步的状态,否则会漏选或多选元素;

  3. 剩余元素处理:newArr是通过filter生成的新数组,不是修改原arr,这样能保证每次递归的剩余元素都是独立的,不会相互影响。

五、思路拓展:时间复杂度与空间复杂度

了解复杂度,能帮我们更好地理解算法的效率:

  • 时间复杂度:O(n!) ------ n是数组长度,全排列的总数是n!,每个排列的构建需要O(n)时间,整体就是O(n×n!),简化为O(n!);

  • 空间复杂度:O(n) ------ 递归栈的深度是n(最多递归n层),temp数组的长度最多也是n,res数组存储所有排列,属于输出空间,一般不计入复杂度。

六、总结

LeetCode 46. 全排列的核心是「回溯法+DFS」,记住一句话:"选一个元素,递归穷举剩余,回溯回退换另一个"

这道题的代码很简洁,但每一步都体现了回溯的思想,尤其是temp.pop()的回退操作,是新手理解回溯的关键。建议大家自己动手调试一遍,看着temp和arr的变化,就能彻底明白回溯的逻辑。

相关推荐
小小小小宇3 小时前
前端转后端基础- 变量和类型
前端
穿条秋裤到处跑4 小时前
每日一道leetcode(2026.03.31):字典序最小的生成字符串
算法·leetcode
Cobyte4 小时前
1.基于依赖追踪和触发的响应式系统的本质
前端·javascript·vue.js
主宰者4 小时前
C# CommunityToolkit.Mvvm全局事件
java·前端·c#
前端小咸鱼一条5 小时前
16.迭代器 和 生成器
开发语言·前端·javascript
小江的记录本5 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web
web守墓人5 小时前
【前端】记一次将ruoyi vue3 element-plus迁移到arco design vue的经历
前端·vue.js·arco design
伊步沁心5 小时前
Webpack & Vite 深度解析
前端
libokaifa5 小时前
OpenSpec + TDD:让 AI 写代码,用测试兜底
前端·ai编程
用户15815963743705 小时前
搭 AI Agent 团队踩了 18 个坑,总结出这 5 个关键步骤
前端