LeetCode Hot100(37/100)——46. 全排列

文章目录

一、题目理解

  • 描述:给定一个不含重复数字的数组 nums,返回其所有可能的全排列。
  • 要点:
    • 元素互不相同
    • 需要列出所有排列,顺序不限
    • 输出是所有排列的集合,每个排列长度等于数组长度

示例

  • 输入:nums = 1,2,3
  • 输出:\[1,2,3,1,3,2,2,1,3,2,3,1,3,1,2,3,2,1]

二、思路总览

全排列解法总览
回溯 + used数组
原理: 逐位选择未使用数字
状态: 路径path, 标记used
终止: path长度==n
时间: O(n·n!)
空间: O(n)(不含结果)
回溯 + 原地交换
原理: 固定位置first, 与后面位置交换
优点: 无需额外used数组
时间: O(n·n!)
空间: O(n)(递归栈, 原地操作)
迭代插入法
原理: 逐个数字插入已有排列的每个位置
时间: O(n·n!)
空间: O(n·n!)(构建过程中)
字典序 next_permutation
原理: 先排序, 反复调用下一个排列
前提: 所有数字不同
时间: O(n·n!)
空间: O(1)(原地, 不含结果)

三、最常用方法:回溯 + used 数组

1. 核心思想

  • 把排列构建看作一条"路径"。每一层递归在当前路径后面选择一个还没用过的数字,直到路径长度等于 n。
  • 三要素:
    • 选择:挑一个未使用的数字 numsi
    • 约束:usedi 为 false 才能选
    • 结束:path.size() == n 时收集答案

2. 算法流程图





开始
初始化 path, used, res
path.size()==n?
收集 path 的拷贝到 res
返回上一层
遍历 i=0..n-1
usedi==false?
选择: usedi=true, path.add(numsi)
撤销: path.removeLast(), usedi=false

3. 复杂度

  • 时间:O(n · n!)。共有 n! 个排列,每次生成/拷贝一条路径成本 O(n)。
  • 空间:O(n) 递归栈 + O(n) used 数组(不含结果存储)。

4. Java 代码

java 复制代码
import java.util.*;

public class SolutionUsed {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        Deque<Integer> path = new ArrayDeque<>();
        backtrack(nums, used, path, res);
        return res;
        // 结果包含 n! 条列表,每条长度 n
    }

    private void backtrack(int[] nums, boolean[] used, Deque<Integer> path, List<List<Integer>> res) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            // 选择
            used[i] = true;
            path.addLast(nums[i]);
            // 递归
            backtrack(nums, used, path, res);
            // 撤销选择
            path.removeLast();
            used[i] = false;
        }
    }
}

四、原地交换回溯(更节省辅助空间)

1. 核心思想

  • 用 first 表示当前需要固定的位置,从 first 到末尾依次把每个数字换到 first,再递归固定下一个位置。
  • 递归返回后再交换回来(恢复现场)。

2. 特点

  • 无需 used\[\],用交换保证位置不重复使用。
  • 对原数组进行原地变换,空间更省。

3. 复杂度

  • 时间:O(n · n!)
  • 空间:O(n) 递归栈(原地操作没有额外结构)

4. Java 代码

java 复制代码
import java.util.*;

public class SolutionSwap {
    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> perm = new ArrayList<>(nums.length);
            for (int v : nums) perm.add(v);
            res.add(perm);
            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[] a, int i, int j) {
        if (i == j) return;
        int t = a[i]; a[i] = a[j]; a[j] = t;
    }
}

五、迭代构造(逐步插入法)

1. 核心思想

  • 从空排列开始,依次把 nums 的每个数字,插入到当前所有排列的每一个位置,构成新的全体排列。
  • 例如:已有 \[1,2 ],插入 3 得到 \[3,1,2, 1,3,2, 1,2,3 ]。

2. 复杂度

  • 时间:O(n · n!)。每加入一个数,需要对当前所有排列进行 O(长度) 次插入。
  • 空间:O(n · n!)。需要存放所有中间与最终结果。

3. Java 代码

java 复制代码
import java.util.*;

public class SolutionIterativeInsert {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        res.add(new ArrayList<>()); // 从空排列开始
        for (int num : nums) {
            List<List<Integer>> next = new ArrayList<>();
            for (List<Integer> perm : res) {
                for (int pos = 0; pos <= perm.size(); pos++) {
                    List<Integer> newPerm = new ArrayList<>(perm);
                    newPerm.add(pos, num);
                    next.add(newPerm);
                }
            }
            res = next;
        }
        return res;
    }
}

六、字典序生成:next_permutation

1. 核心思想

  • 先按升序排序 nums,即得到字典序最小的排列。
  • 反复调用"下一个排列"的原地算法,直到没有下一个为止。
  • 下一个排列步骤:
    1. 从右往左找到第一个 numsi < numsi+1 的位置 i
    2. 从右往左找到第一个 numsj > numsi 的位置 j
    3. 交换 numsi, numsj
    4. 反转区间 i+1, n-1

2. 复杂度

  • 单次 next_permutation:O(n)
  • 总时间:O(n · n!)(要生成 n! 次)
  • 额外空间:O(1)(原地,不含结果)

3. Java 代码

java 复制代码
import java.util.*;

public class SolutionNextPermutation {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        do {
            List<Integer> cur = new ArrayList<>(nums.length);
            for (int v : nums) cur.add(v);
            res.add(cur);
        } while (nextPermutation(nums));
        return res;
    }

    // 返回是否存在下一个排列
    private boolean nextPermutation(int[] a) {
        int n = a.length;
        // 1) 寻找下降点
        int i = n - 2;
        while (i >= 0 && a[i] >= a[i + 1]) i--;
        if (i < 0) return false; // 已是最大排列
        // 2) 从右找第一个 > a[i] 的
        int j = n - 1;
        while (a[j] <= a[i]) j--;
        // 3) 交换
        swap(a, i, j);
        // 4) 反转后缀
        reverse(a, i + 1, n - 1);
        return true;
    }

    private void swap(int[] a, int i, int j) {
        int t = a[i]; a[i] = a[j]; a[j] = t;
    }
    private void reverse(int[] a, int l, int r) {
        while (l < r) swap(a, l++, r--);
    }
}

七、过程可视化示例

以回溯 + used 数组方法,nums = 1,2,3,状态树(部分展开):
开始: path=\[\]
path=1
path=2
path=3
path=1,2
path=1,3
path=1,2,3 收集
path=1,3,2 收集
path=2,1
path=2,3
path=3,1
path=3,2

next_permutation 生成 1,2,31,3,22,1,32,3,13,1,23,2,1 的时序过程:
数组 next_permutation 数组 next_permutation 初始 1,2,3 得到 1,3,2 依次得到 2,1,3, 2,3,1, 3,1,2, 3,2,1 找 i: 从右往左第一个 ai < ai+1 找 j: 从右往左第一个 aj > ai 交换 ai, aj 反转 i+1, 末尾 重复上述步骤 返回 false 时结束


  • 全排列的本质是对长度为 n 的位置进行全排列搜索,典型解法是回溯。
  • 四种常见实现:
    • 回溯 + used 数组(最通用)
    • 原地交换回溯(空间节省)
    • 迭代插入法(非递归)
    • 字典序 next_permutation(顺序生成)
  • 复杂度下界由输出规模决定:时间 O(n · n!),空间至少要容纳 n! 条结果。选择实现时更多考虑代码风格、可读性和使用场景。
相关推荐
BothSavage14 小时前
Trae远程开发中DeepSeek自定义模型4054错误的排查与修复
算法
小林ixn14 小时前
从暴力到KMP:一道题彻底搞懂字符串匹配的前世今生
算法
烬羽15 小时前
字符串算法入门:从反转字符串到回文判断,面试不再慌
算法·面试
先吃饱再说1 天前
判断回文字符串,从一行代码到双指针优化
算法
黄敬峰1 天前
深入理解算法核心:从递归思想、数组扁平化到快速排序
算法
得物技术1 天前
从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理
人工智能·算法·架构
AI小老六2 天前
SkillOpt 架构拆解:把 Skill 文本当参数,用执行轨迹训练 Agent
后端·算法·ai编程
胡萝卜术2 天前
从“分数打架”到“排名投票”:为什么你的ChatBI必须用RRF?
算法·设计模式·面试
Asize2 天前
初识DFS 与 BFS:递归、队列与图遍历
算法