【回溯-4】46.全排列

题目描述:

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

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

示例 2:

复制代码
输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

复制代码
输入:nums = [1]
输出:[[1]]

解题思路:

方法一:回溯 + used 数组

核心思路:

把问题看成树形结构:

  • 每一层选择一个数字

  • 每个数字只能用一次

  • 用 used 数组标记哪些数字已经被选

具体过程示例:

nums = [1,2,3]

复制代码
                    []
          /         |         \
         1          2          3
        / \        / \        / \
       2   3      1   3      1   2
       |   |      |   |      |   |
       3   2      3   1      2   1

所有路径:
1→2→3, 1→3→2
2→1→3, 2→3→1
3→1→2, 3→2→1

代码实现:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> path;
        vector<bool> used(nums.size(), false);
        backtrack(nums, used, path, result);
        return result;
    }
    
private:
    void backtrack(vector<int>& nums, vector<bool>& used,
                   vector<int>& path, vector<vector<int>>& result) {
        // 终止条件:path 长度等于 nums 长度
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        
        // 遍历所有数字
        for (int i = 0; i < nums.size(); i++) {
            if (used[i]) continue;  // 已使用,跳过
            
            used[i] = true;              // 标记已使用
            path.push_back(nums[i]);     // 选择
            backtrack(nums, used, path, result);  // 递归
            path.pop_back();             // 撤销
            used[i] = false;             // 取消标记
        }
    }
};

复杂度分析:

设 n 是数组长度。

维度 复杂度 说明
时间复杂度 O(n × n!) n! 个排列,每个排列长度 n
空间复杂度 O(n) 递归栈深度 + path + used

关键细节:

  1. 为什么用 used 数组?

因为每个元素只能用一次,需要标记哪些元素已经被选。

  • used[i] = true:表示 nums[i] 已经被选

  • used[i] = false:表示 nums[i] 还没被选

  1. 为什么 path.size() == nums.size() 是终止条件?

当 path 长度等于 nums 长度时,说明所有元素都被选了,得到一个完整排列。

  1. 和「组合总和」的区别
题目 区别
39. 组合总和 可重复选,用 start 避免重复组合
46. 全排列 不可重复选,用 used 标记已选
78. 子集 不可重复选,用 start 控制顺序

关键区别:

  • 组合:顺序不重要,用 start 避免重复

  • 排列:顺序重要,用 used 标记已选

方法二:交换法(不需要 used 数组)

思路:把数组分成"已选"和"未选"两部分,通过交换把未选的元素换到当前位置。

代码实现:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> result;
        backtrack(nums, 0, result);
        return result;
    }
    
private:
    void backtrack(vector<int>& nums, int start, vector<vector<int>>& result) {
        if (start == nums.size()) {
            result.push_back(nums);
            return;
        }
        
        for (int i = start; i < nums.size(); i++) {
            swap(nums[start], nums[i]);           // 选择
            backtrack(nums, start + 1, result);   // 递归
            swap(nums[start], nums[i]);           // 撤销
        }
    }
};

优点 :不需要 used 数组,空间更省。

缺点:修改了原数组(但会恢复)。

方法三:插入法(迭代)

思路:逐个插入元素,生成所有排列。

代码实现:

cpp 复制代码
class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> result = {{}};
        
        for (int num : nums) {
            vector<vector<int>> next;
            for (auto& perm : result) {
                for (int i = 0; i <= perm.size(); i++) {
                    vector<int> newPerm = perm;
                    newPerm.insert(newPerm.begin() + i, num);
                    next.push_back(newPerm);
                }
            }
            result = move(next);
        }
        
        return result;
    }
};

复杂度:时间 O(n × n!),空间 O(n × n!)

三种方法对比:

方法 时间复杂度 空间复杂度 是否修改原数组 推荐度
回溯 + used O(n × n!) O(n) ❌ 不修改 ⭐⭐⭐⭐⭐
交换法 O(n × n!) O(n) ✅ 修改(会恢复) ⭐⭐⭐⭐
插入法 O(n × n!) O(n × n!) ❌ 不修改 ⭐⭐⭐

总结:

要点 说明
核心思想 回溯:每层选一个未使用的数字
关键操作 used[i] = true → 递归 → used[i] = false
终止条件 path.size() == nums.size()
时间复杂度 O(n × n!)
空间复杂度 O(n)
相关推荐
Rabitebla12 小时前
【Linux】进程控制(十二) 自定义 Shell(二):内建命令与进程工作目录管理
linux·c++·笔记·学习·bash
码客-ygluu17 小时前
一个通用游戏数据服务器
c++·架构
another heaven17 小时前
【C/C++ Ranges 是零开销抽象吗?拆开管道、惰性求值和编译期膨胀】
c语言·开发语言·c++
Code_Solitude17 小时前
C语言动态内存 + 柔性数组 + 数组名退化
c语言·算法·柔性数组
上海潭龙东海科技有限公司18 小时前
客运站管控之动态孪生技术方案(新增一屏管控模块
数据仓库·人工智能·算法·重构·动态规划
文丑颜不良啊19 小时前
LeetCode2821~2840题目解析
java·leetcode
YLCHUP20 小时前
【思维&图论建模】题解:债权(debt)_图论_建模_思路转化_思维题目_C++算法竞赛
c++·笔记·算法·数学建模·贪心算法·深度优先·图论
xiaoye-duck20 小时前
《C++ 项目:手写大模型接入 SDK》会话模块整合:内存 SessionManager 与 DataManager 持久化联动
开发语言·数据库·c++·语言模型
思麟呀20 小时前
背包DP(01背包+完全背包)
c++·算法