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]]

三、错误代码

java 复制代码
class Solution {
    List<List<Integer>> res=new ArrayList<>();
    boolean[] used=new boolean[nums.length]; // 致命错误1
    List<Integer>path=new ArrayList<>();

    public List<List<Integer>> permute(int[] nums) {
        if(nums==null||nums.length==0){
            return res;
        }
        back(nums);
        return res;
    }

    private void back(int[] nums){
        if(path.size()==nums.length){
            res.add(new ArrayList<>(path));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(nums[i]==true){ // 致命错误2
                continue;
            }
            if(nums[i]==false){ // 致命错误3
                path.add(nums[i]);
                used[i]=true;
                back(nums);
                path.remove(pathsize()-1); // 致命错误4
                used[i]=false;
            }
        }
    }
}

四、逐条复盘我的所有错误

错误1:类成员位置直接 new used 数组

boolean[] used=new boolean[nums.length];

原因:nums 是方法参数,类加载的时候 nums 还不存在,不能用来初始化成员变量。

正确做法:只声明不初始化,在主方法里根据 nums 长度创建数组。

错误2、3:把 nums[i] 当成布尔标记判断

if(nums[i]==true)、if(nums[i]==false)

超级低级逻辑错误:

nums[i] 是数字(1、2、3),不是布尔值!

是否被使用,看的是usedi,不是 numsi。

正确逻辑 :如果 used[i]==true 说明用过了,直接跳过。

错误4:拼写错误 pathsize()

集合获取长度是 path.size(),不能连写,会编译报错。

错误5:多余嵌套 if

只要判断 used[i] 为 true 就 continue,剩下的一定是未使用,不需要再套一层 if 判断 false。

五、标准代码

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    // 结果集、当前路径、标记数组(只声明)
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used;

    public List<List<Integer>> permute(int[] nums) {
        if(nums == null || nums.length == 0){
            return res;
        }
        // 根据输入数组长度初始化标记数组
        used = new boolean[nums.length];
        back(nums);
        return res;
    }

    // 回溯核心函数
    private void back(int[] nums){
        // 终止条件:路径长度等于数组长度,说明凑齐一组排列
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }

        // 遍历所有数字,逐个尝试选择
        for(int i = 0; i < nums.length; i++){
            // 已经选过的数字,跳过
            if(used[i]){
                continue;
            }

            // 1. 做选择:加入路径 + 标记占用
            path.add(nums[i]);
            used[i] = true;

            // 2. 递归:继续选下一个位置的数
            back(nums);

            // 3. 回溯(撤销选择):递归跑完回来,恢复状态
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }
}

六、重点攻克:我最看不懂的四行回溯代码

这四行是回溯的灵魂,也是我最开始完全不懂的地方:

java 复制代码
path.add(nums[i]);
used[i]=true; 
back(nums);
path.remove(path.size()-1);
used[i]=false;

通俗大白话解释(终于听懂版)

第一步:做选择

把当前数字放进路径,标记为已使用,下层递归不能再选。

第二步:递归深挖

程序直接扎进下一层,继续填下一个位置,直到路径填满、记录答案。

第三步:递归返回,必须回溯

当这一条分支的所有排列全部找完了,回到当前行:

删掉刚刚加的数字、取消占用标记,清空状态,换下一个数字重新选。

最简例子走一遍 [1,2]

  1. 选1 → path=1,used0=true,递归深入

  2. 下层只能选2 → path=1,2,存答案

  3. 递归返回,删掉2、取消标记

  4. 循环走完,回到最上层,删掉1、取消标记

  5. 状态清空,开始选2开头的排列

不回溯 = 状态永远不重置 = 只能算出第一个答案

七、回溯核心模板(永久背诵)

所有排列题通用模板:

终止条件:收集答案

for 遍历所有选择:

if 已使用 → 跳过

做选择

递归

撤销选择(回溯)

八、总结

  1. used 是标记下标,和 nums 数组本身无关,绝对不能拿 nums 判断布尔值。

  2. 成员变量不能用方法参数初始化,used 必须在主函数 new。

  3. 回溯核心就是:选进去 → 递归到底 → 退出来重置。

  4. 存答案必须 new ArrayList<>(path),保存快照,不能直接存 path。

  5. 撤销顺序必须和选择顺序相反: 选择:add → true 撤销:remove → false

相关推荐
潼心1412o33 分钟前
C++初阶(长期更新)第6讲:类和对象(下)
开发语言·c++·算法
星光开发者1 小时前
基于决策树随机森林算法的小说推荐系统的设计与实现-计算机毕设【课程设计】78485
vue.js·算法·微信小程序·django·php·课程设计·express
-森屿安年-1 小时前
按身高排序
c++·算法
程序员清风1 小时前
算法与数据结构入门:复杂度、数组与链表
数据结构·算法·链表
无定义_2 小时前
Codeforces Round 1124 (Div. 2)
算法
垆边人似月.3 小时前
任务混部(200 分档)
算法
言乐63 小时前
Python区分广度优先深度优先宽度优先的区别
python·算法·深度优先·广度优先·宽度优先
Rnan-prince3 小时前
LeetCode 股票买卖系列:一个动态规划状态机模板通吃四题
leetcode·动态规划
如意猴3 小时前
【C++】007 C/C++ 内存管理机制、malloc与new的区别及模板初阶
java·c++·算法
朝朝辞暮i3 小时前
C++ 第 32 课:main()、rclcpp::init()、spin()、shutdown()
开发语言·c++·算法