【前端面试常见算法题系列】46. 全排列(中等)

165 阅读3分钟

“Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。”

一、题目描述

给定一个不含重复数字的数组 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]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

二、思路分析

这道题有点像我们高中时期的排列组合,给一个数组,然后将里面的元素随意打乱,看最终能有多少种不同的组合。那么要怎么列举出这些组合呢,我们肯定不会无规律地乱穷举,一般会这样想:先固定第一位,然后第二位是 2 ,第三位是 3 ;然后把第二位变成 3 ,第三位变成 2 ;然后把第一位换成 2 ,重复这种过程。

其实这就是回溯算法,本质上是一种暴力穷举算法。那么我们该怎么用代码实现这种算法呢?思考一下,我们是不是会:

  • 将第一位这样变化 1或2或3
  • 将第二位这样变化 2或3或1
  • 将第三位这样变化 3或1或2 那么首先 for 循环肯定是跑不掉的了,那么就是第一位进行 for 循环;在第一位的 for 循环里进行第二位的 for 循环,此时需要注意第二位不能与第一位相同,找到第一个不与第一位相同的元素后,就进行第三位的 for 循环,此时需要注意不能与前两位相同,然后找到第一个不与前两位相同的元素即可。

思考到这里了,已经有点眉目了。我们肯定不会这样写:

for () {
    for () {
        for () {}
    }
}

聪明的你肯定已经想到递归调用自身,因此我们可以

function dfs(nums) {
    for (let i = 0; i < nums.length; i++) {
        dfs(nums);
    }
}

有递归当然就有结束递归。由于最后返回的是数组,数组里的元素是排序组合后的小数组,因此可以全局定义一个数组 arr 用来存储最后返回的结果,定义一个 track 数组用来存储小数组,当小数组的长度为 nums.length 时,就将小数组压入 arr ,这就是结果递归的判断了。于是变成:

const arr = [], track = [];
function dfs (nums) {
    if (track.length === nums.length) {
        arr.push(Array.from(track));
        return;
    }
    for (let i = 0; i < nums.length; i++) {
        dfs(nums);
    }
}

下面就是 for 循环里的数据处理了。在循环里要做什么呢?如果当前元素不与上一位相同就压入数组,如果相同就继续遍历。那如何判断元素是否相同呢?用 bool 数组判断,将当前元素压入栈后,bool 数组中下标为当前元素的值赋为 true ,也就是说循环时判断 bool 数组中下标为当前元素的值是否为 true。那么递归函数变成:

const arr = [], track = [];
function dfs (nums, used = []) {
    if (track.length === nums.length) {
        arr.push(Array.from(track));
        return;
    }
    for (let i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        track.push(nums[i]);
        used[i] = true;
        dfs(nums, used);
    }
}

递归返回后,此时 track 数组已经满了,因此需要将元素取出来,并且每取一个出来就要将 used 数组里对应元素修改为 false 。因此:

const arr = [], track = [];
function dfs (nums, used = []) {
    if (track.length === nums.length) {
        arr.push(Array.from(track));
        return;
    }
    for (let i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        track.push(nums[i]);
        used[i] = true;
        dfs(nums, used);
        track.pop();
        used[i] = false;
    }
}

至此,终于结束这道题了,最后一步如果还不理解为什么的话,这里贴下 debug 细节:

image.png

三、AC 代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    const arr = [], track = [];
    const dfs = function(nums, k, used) {
        if (track.length === k) {
            arr.push(Array.from(track));
            return;
        }
        for (let i = 0; i < k; i++) {
            if (used[i]) continue;
            track.push(nums[i]);
            used[i] = true;
            dfs(nums, k, used);
            track.pop();
            used[i] = false;
        }
    }
    dfs(nums, nums.length, []);
    return arr;
};