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的变化,就能彻底明白回溯的逻辑。

相关推荐
IT_陈寒2 小时前
Vite 凭什么比 Webpack 快50%?揭秘闪电构建背后的黑科技
前端·人工智能·后端
颜酱2 小时前
Dijkstra 算法:从 BFS 到带权最短路径
javascript·后端·算法
hi大雄2 小时前
我的 2025 —— 名为《开始的勇气》🌱
前端·年终总结
从文处安3 小时前
「前端何去何从」一直写 Vue ,为何要在 AI 时代去学 React?
前端·react.js
aircrushin3 小时前
OpenClaw“养龙虾”现象的社会技术学分析
前端·后端
明君879973 小时前
#Flutter 的官方Skills技能库
前端·flutter
yuki_uix3 小时前
重新认识 React Hooks:从会用到理解设计
前端·react.js
林太白3 小时前
ref和reactive对比终于学会了
前端
Apifox3 小时前
测试数据终于不用到处复制了,Apifox 自动化测试新增「共用测试数据」
前端·后端·测试