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

相关推荐
wzdark2 小时前
基于堆的优先队列实现原理与复杂度分析4
算法
老王熬夜敲代码4 小时前
BLAKE3:最快的密码学哈希函数
算法·密码学·哈希算法
hansang_IR4 小时前
【题解】P4460 [CQOI2018] 解锁屏幕
c++·算法
见闻小天地4 小时前
简博斯JG-C系列彩色激光同轴位移计:摄像头模组对位与行程测量的技术价值观察
人工智能·算法
葫三生5 小时前
《论三生原理》神话学构想与“神话历史”理论、《古史中的神话》思路异同?
大数据·人工智能·科技·深度学习·算法
liliangcsdn5 小时前
因子分析指标概念与示例
算法·机器学习
SupL!6 小时前
LoftQ原理
算法
Navigator_Z8 小时前
LeetCode //C - 1240. Tiling a Rectangle with the Fewest Squares
c语言·算法·leetcode
稻米哟8 小时前
力扣100——双指针
算法·leetcode
明志数科9 小时前
具身智能真机采集工程实践:5类高频失效模式与规避清单
人工智能·算法·机器学习