LeetCode-44 回溯解法

全排列:一篇看懂回溯解法

题目

给定一个不含重复数字的整数数组 nums,返回它的所有全排列。

示例

java 复制代码
输入:nums = [1,2,3]

输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

一、题目

这道题的核心是:

把数组中的每个数字,都轮流放到当前位置上,直到所有位置都放满。

比如 nums = [1,2,3]:

  • 第一位可以放 1
  • 第一位也可以放 2
  • 第一位还可以放 3

每确定一位,就继续递归处理下一位。

这就是典型的 回溯 问题。


二、最容易想到的思路

可以这样理解:

  • 先确定第 0 个位置放谁
  • 再确定第 1 个位置放谁
  • 再确定第 2 个位置放谁
  • 当所有位置都确定后,就得到一个排列

为了高效,我们通常使用 交换 的方式,而不是每次都新建数组。


三、回溯解法

代码

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

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, 0, res);
        return res;
    }

    private void backtrack(int[] nums, int first, List<List<Integer>> res) {
        if (first == nums.length) {
            List<Integer> list = new ArrayList<>();
            for (int num : nums) {
                list.add(num);
            }
            res.add(list);
            return;
        }

        for (int i = first; i < nums.length; i++) {
            swap(nums, first, i);              // 选择一个数放到当前位置
            backtrack(nums, first + 1, res);   // 递归处理下一个位置
            swap(nums, first, i);              // 恢复现场
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

四、为什么这样做可行

假设数组是:

java 复制代码
[1,2,3]

第一步:确定第 0 位

  • 把 1 放到第 0 位,数组还是 [1,2,3]
  • 把 2 放到第 0 位,数组变成 [2,1,3]
  • 把 3 放到第 0 位,数组变成 [3,2,1]

第二步:递归确定后面的位

例如已经确定第 0 位是 1,那后面就去处理 [2,3] 的排列。

最后会得到:

  • [1,2,3]
  • [1,3,2]

再继续处理第 0 位是 2、第 0 位是 3 的情况。


五、为什么交换后还要交换回来

这是回溯最关键的一步。

例如当前数组是:

java 复制代码
[1,2,3]

当我们尝试把 2 放到第 0 位时,会执行:

java 复制代码
swap(nums, 0, 1)

数组变成:

java 复制代码
[2,1,3]

递归处理完以后,如果不换回来,数组就一直是 [2,1,3],会影响后面继续尝试别的情况。

所以必须再交换一次:

java 复制代码
swap(nums, 0, 1)

把数组恢复成原来的样子:

java 复制代码
[1,2,3]

这样下一轮尝试才不会出错。

一句话概括:

交换过去是做选择,交换回来是撤销选择。


六、执行过程示例

以 nums = [1,2,3] 为例:

第一层:确定第 0 位

  • 选 1,得到 [1,2,3]
  • 选 2,得到 [2,1,3]
  • 选 3,得到 [3,2,1]

第二层:确定第 1 位

例如当前是 [1,2,3]:

  • 选 2,得到 [1,2,3]
  • 选 3,得到 [1,3,2]

第三层:确定第 2 位

只剩最后一个数,直接加入结果。

最终得到全部 6 种排列。


七、复杂度分析

时间复杂度

text 复制代码
O(n × n!)

原因:

  • 一共有 n! 种排列
  • 每次加入答案时,需要把当前数组复制到结果中,花费 O(n)

空间复杂度

text 复制代码
O(n)

主要是递归调用栈的深度。

不计最终答案所占空间。


八、这道题的本质

这道题的本质不是"交换",而是:

枚举每个位置可以放哪些数。

交换只是一个实现手段,它的优点是:

  • 不需要额外创建很多新数组
  • 代码更简洁
  • 性能更好

九、总结

这道题是经典的回溯题。

可以记住下面这个模板:

java 复制代码
做选择
递归
撤销选择

对应到本题就是:

java 复制代码
swap(nums, first, i);
backtrack(nums, first + 1, res);
swap(nums, first, i);

其中:

  • swap:把一个数放到当前的位置
  • backtrack:递归处理下一个位置
  • 再 swap:恢复现场,继续尝试别的数

十、结论

全排列问题最经典的解法就是 回溯 + 交换。

它的核心思想是:

  • 每一层确定一个位置放什么
  • 通过交换把候选数字放到当前位置
  • 递归处理剩余位置
  • 处理完再恢复原数组
相关推荐
hanlin034 小时前
刷题笔记:力扣第32题-最长有效括号
笔记·算法·leetcode
All for pursuit.5 小时前
【动态规划-21】312.戳气球
数据结构·c++·算法·leetcode·动态规划
碧海银沙音频科技研究院7 小时前
基于炬芯ATS3089X自研AEC声学回声消除算法
算法
通问AI7 小时前
在两台 Mac Studio 上跑 800B 模型:MLX + RDMA over Thunderbolt 实测与四道门选型法
算法
ZywOo6237 小时前
《算法竞赛进阶指南》0x03 前缀和与差分
算法
Scintilla_y7 小时前
DAY2 滑动窗口
算法·leetcode·散列表
lvwangshu7 小时前
笛卡尔树
算法·图论
026d238 小时前
状态信号只知“其然”不知“其所以然”:变工况机械故障诊断的物理盲区
算法
姜鱼问生8 小时前
备份和服务器在同一台机器上,等于没备份
算法·架构
姜鱼问生8 小时前
别再 chmod 777 了,用 ACL 给它开一扇窗
后端·算法·架构