【LeetCode Hot100 刷题日记 (55/100)】46. 全排列 —— 回溯算法(Backtracking)🔥

3 阅读5分钟

📌 题目链接:46. 全排列 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:数组、回溯、递归

⏱️ 目标时间复杂度:O(n × n!)

💾 空间复杂度:O(n)


🧠 题目分析

给定一个不含重复数字的整数数组 nums,要求返回其所有可能的全排列。全排列即对数组中所有元素进行重新排序,使得每种顺序都恰好出现一次。

  • 输入特点:无重复元素、长度 ≤ 6(小规模)
  • 输出要求:所有排列组合,顺序任意
  • 关键限制:每个数字只能使用一次

这是一道经典的组合类搜索问题,非常适合用回溯法(Backtracking) 解决。


🔁 核心算法及代码讲解:回溯(Backtracking)

✅ 什么是回溯?

回溯是一种暴力搜索 + 剪枝优化的算法思想,常用于解决排列、组合、子集、N皇后等问题。其核心是:

  • 尝试:在当前状态做出一个选择;
  • 递归:进入下一层状态继续探索;
  • 撤销:回退到上一状态,尝试其他选择(即“回溯”)。

🎯 回溯 = DFS(深度优先搜索) + 状态重置

✅ 本题回溯策略

我们把排列过程看作依次填满 n 个位置

  • 第 0 位:从所有数字中选一个;
  • 第 1 位:从剩下的数字中选一个;
  • ...
  • 第 n-1 位:只剩最后一个数字。

为了高效实现“剩下的数字”,官方题解采用了一种巧妙的原地交换法,避免额外使用 visited 数组:

  • 将数组划分为两部分:

    • [0, first - 1]:已确定的前缀(已选)
    • [first, n - 1]:可选的候选集(未选)
  • 每次从候选集中选一个数 nums[i],与 nums[first] 交换,使其进入前缀;

  • 递归处理 first + 1

  • 回溯时再交换回来,恢复原状。

💡 这种方法空间更优(无需额外 visited),但不保证字典序输出。若题目要求字典序,需先排序或用 visited 数组。

✅ 代码逐行注释(C++)

void backtrack(vector<vector<int>>& res, vector<int>& output, int first, int len) {
    // 🛑 终止条件:所有位置已填满(first == len)
    if (first == len) {
        res.emplace_back(output);  // 将当前排列加入结果集
        return;
    }
    // 🔁 枚举所有可选的数字(从 first 到 len-1)
    for (int i = first; i < len; ++i) {
        // 🔄 交换:将 nums[i] 放到第 first 位(选它!)
        swap(output[i], output[first]);
        // 📥 递归:处理下一个位置 first+1
        backtrack(res, output, first + 1, len);
        // 🔙 回溯:撤销选择,恢复原数组状态
        swap(output[i], output[first]);
    }
}

⚠️ 注意:output 是引用传递,因此必须在递归后显式撤销操作,否则会影响其他分支!


🧩 解题思路(分步拆解)

  1. 初始化:准备结果容器 res,直接复用输入 nums 作为工作数组。

  2. 递归入口:从第 0 位开始填,调用 backtrack(res, nums, 0, n)

  3. 递归过程

    • first == n,说明已生成一个完整排列,加入 res

    • 否则,遍历 [first, n-1] 中的每个位置 i

      • 交换 nums[first]nums[i],使 nums[i] 被“选中”;
      • 递归填 first + 1
      • 交换回来,恢复现场,尝试下一个 i
  4. 返回结果res 包含所有全排列。

✅ 此方法利用了排列的对称性:通过交换,动态维护“已选”和“未选”区域。


📊 算法分析

⏱️ 时间复杂度:O(n × n!)

  • 共有 n! 种排列;
  • 每次生成一个排列需要 O(n) 时间(复制到结果集);
  • 递归调用总次数约为 e × n!(数学上 ∑P(n,k) < 3n!),仍为 O(n!) 级别;
  • 总体:O(n × n!)

💾 空间复杂度:O(n)

  • 递归栈深度:最多 n 层(每个位置递归一次)→ O(n)
  • 结果存储:O(n × n!),但通常不计入空间复杂度(题目要求输出)
  • 额外空间:仅用几个变量 → O(1) 辅助空间

✅ 该解法是空间最优的回溯实现之一。


💻 完整代码

C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    void backtrack(vector<vector<int>>& res, vector<int>& output, int first, int len) {
        // 所有数都填完了
        if (first == len) {
            res.emplace_back(output);
            return;
        }
        for (int i = first; i < len; ++i) {
            // 动态维护数组
            swap(output[i], output[first]);
            // 继续递归填下一个数
            backtrack(res, output, first + 1, len);
            // 撤销操作
            swap(output[i], output[first]);
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int> > res;
        backtrack(res, nums, 0, (int)nums.size());
        return res;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> nums1 = {1, 2, 3};
    auto res1 = sol.permute(nums1);
    for (auto& p : res1) {
        for (int x : p) cout << x << " ";
        cout << "\n";
    }
    cout << "---\n";

    vector<int> nums2 = {0, 1};
    auto res2 = sol.permute(nums2);
    for (auto& p : res2) {
        for (int x : p) cout << x << " ";
        cout << "\n";
    }
    cout << "---\n";

    vector<int> nums3 = {1};
    auto res3 = sol.permute(nums3);
    for (auto& p : res3) {
        for (int x : p) cout << x << " ";
        cout << "\n";
    }

    return 0;
}

JavaScript

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    const res = [];
    
    function backtrack(first) {
        if (first === nums.length) {
            res.push([...nums]); // 深拷贝当前排列
            return;
        }
        for (let i = first; i < nums.length; i++) {
            // 交换
            [nums[first], nums[i]] = [nums[i], nums[first]];
            // 递归
            backtrack(first + 1);
            // 回溯
            [nums[first], nums[i]] = [nums[i], nums[first]];
        }
    }
    
    backtrack(0);
    return res;
};

// 测试
console.log(permute([1,2,3]));
console.log(permute([0,1]));
console.log(permute([1]));

💡 JS 注意:res.push([...nums]) 必须用展开运算符深拷贝,否则所有结果会指向同一个数组引用!


🎯 面试高频考点 & 延伸思考

❓ 面试常问问题

  1. 为什么需要两次 swap?

    • 第一次 swap 是“做选择”,第二次是“撤销选择”,保证回溯后状态干净。
  2. 能否不用 swap,用 visited 数组?

    • 可以!但空间复杂度变为 O(n),适合需要字典序输出含重复元素的情况(如 LeetCode 47)。
  3. 时间复杂度为什么是 O(n × n!)?

    • 因为要复制 n! 个长度为 n 的排列到结果中。
  4. 如果数组有重复元素怎么办?

    • 需要去重:排序 + 跳过相同元素(见 LeetCode 47. 全排列 II)。

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!