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。
  • 三要素:
    • 选择:挑一个未使用的数字 nums[i]
    • 约束:used[i] 为 false 才能选
    • 结束:path.size() == n 时收集答案

2. 算法流程图





开始
初始化 path, used, res
path.size()==n?
收集 path 的拷贝到 res
返回上一层
遍历 i=0..n-1
used[i]==false?
选择: used[i]=true, path.add(nums[i])
撤销: path.removeLast(), used[i]=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. 从右往左找到第一个 nums[i] < nums[i+1] 的位置 i
    2. 从右往左找到第一个 nums[j] > nums[i] 的位置 j
    3. 交换 nums[i], nums[j]
    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,3] → [1,3,2] → [2,1,3] → [2,3,1] → [3,1,2] → [3,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: 从右往左第一个 a[i] < a[i+1] 找 j: 从右往左第一个 a[j] > a[i] 交换 a[i], a[j] 反转 [i+1, 末尾] 重复上述步骤 返回 false 时结束


  • 全排列的本质是对长度为 n 的位置进行全排列搜索,典型解法是回溯。
  • 四种常见实现:
    • 回溯 + used 数组(最通用)
    • 原地交换回溯(空间节省)
    • 迭代插入法(非递归)
    • 字典序 next_permutation(顺序生成)
  • 复杂度下界由输出规模决定:时间 O(n · n!),空间至少要容纳 n! 条结果。选择实现时更多考虑代码风格、可读性和使用场景。
相关推荐
寻寻觅觅☆15 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
偷吃的耗子15 小时前
【CNN算法理解】:三、AlexNet 训练模块(附代码)
深度学习·算法·cnn
化学在逃硬闯CS16 小时前
Leetcode1382. 将二叉搜索树变平衡
数据结构·算法
ceclar12316 小时前
C++使用format
开发语言·c++·算法
Gofarlic_OMS17 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
夏鹏今天学习了吗17 小时前
【LeetCode热题100(100/100)】数据流的中位数
算法·leetcode·职场和发展
忙什么果18 小时前
上位机、下位机、FPGA、算法放在哪层合适?
算法·fpga开发
董董灿是个攻城狮18 小时前
AI 视觉连载4:YUV 的图像表示
算法
ArturiaZ19 小时前
【day24】
c++·算法·图论
大江东去浪淘尽千古风流人物19 小时前
【SLAM】Hydra-Foundations 层次化空间感知:机器人如何像人类一样理解3D环境
深度学习·算法·3d·机器人·概率论·slam