三道全排列面试题

621 阅读4分钟

原型:

给定一个数组,求出数组内元素任意顺序重新排列的所有组合。

初级版:

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

分析:

  • 数组长度为 n,每一位数字,按照排列组合,有 n 的阶乘种可能:n * (n-1) * (n-2) * ... * 3 * 2 * 1 = n!,n 个数字,所以 时间复杂度 为 O(n * n !)
  • 逐个数字设定可能出现的数字,设定到最后一位,即表示本次排列组合完成
  • 再回退到上一步,重复下一种可能的排列组合。 这就是回溯法:一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化抛弃该解,即回溯并且再次尝试。
  • 我们可以将已经确定的数字和后面的数字交换,这样不用额外的存储空间,即可缩小需要排列数字的范围

深度优先搜索

代码如下:

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
  let ans = [];
  backtrack(ans, nums, 0, nums.length);
  return ans;
};

var backtrack = function (ans, curArr, idx, leng) {
  if (idx == leng) {
    ans.push(curArr);
    return;
  }

  for (let i = idx; i < leng; i++) {
    [curArr[i], curArr[idx]] = [...swap(curArr[i], curArr[idx])];
    backtrack(ans, curArr.concat([]), idx + 1, leng);
    [curArr[i], curArr[idx]] = [...swap(curArr[i], curArr[idx])];
  }
}

var swap = function(a, b) {
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;
  return [a, b];
}

进阶版

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

  • 示例 1: 输入:nums = [1,1,2] 输出:[[1,1,2], [1,2,1], [2,1,1]]

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

分析:

  • 方案一:可以先按照无重复的方式,列出来所有的组合,然后自己去重。
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permuteUnique = function(nums) {
  let ans = new Set();
  backtrack(ans, nums, 0);
  let arr = [...ans];
  
  for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i].split('+');
    for (let j = 0; j < arr[i].length; j++) {
      arr[i][j] = parseInt(arr[i][j]);
    }
  }
  return arr;
};

var backtrack = function (result, curArray, curIdx) {
  let leng = curArray.length;

  if (curIdx === leng) {
    result.add(curArray.join('+'));
  }

  for (let i = curIdx; i < leng; i++) {
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
    backtrack(result, curArray.concat([]), curIdx + 1);
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
  }
}

var swap = function (a, b) {
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;
  return [a, b];
}

这种方式执行效率比较低,用时264ms

  • 方法二: 我们发现,如果遇到重复的数字,那么组合后的结果是一样的,那如果遇到重复的,我们只取第一次重复值进行排列,既可以避免重复。为了实现这种思路,需要先解决两个问题:
    1. 首先需要当前元素,后面的元素是升序排列的,这样只要是重复的,就会挨在一起。
    2. 我们需要把组合的数组切成两份,一份是已经组合好的部分,另一份是未组合的部分,对未组合的部分进行排序
let sortArr = curArray.slice(curIdx)
sortArr.sort((a, b) => a - b);
curArray = curArray.slice(0, curIdx).concat(sortArr);
  • 相邻数字的判断就比较容易了
if ( (i < leng - 1 && curArray[i] === curArray[i + 1] ) ) {
  continue;
}

代码如下:

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permuteUnique = function(nums) {
  let ans = [];
  backtrack(ans, nums, 0);
  return ans;
};

var backtrack = function (result, curArray, curIdx) {
  let leng = curArray.length;

  if (curIdx === leng) {
    result.push(curArray);
    return;
  }

  let sortArr = curArray.slice(curIdx);
  sortArr.sort((a, b) => a - b);
  curArray = curArray.slice(0, curIdx).concat(sortArr);

  for (let i = curIdx; i < leng; i++) {
    if ( (i < leng - 1 && curArray[i] === curArray[i + 1] ) ) {
      continue;
    }
    
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
    backtrack(result, curArray.concat([]), curIdx + 1);
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
  }
}

var swap = function (a, b) {
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;
  return [a, b];
}

这种方法的执行效率比方法一好,用时 100ms


高级版

给定正整数 N ,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。 如果我们可以通过上述方式得到 2 的幂,返回 true;否则,返回 false。

  • 示例 1: 输入:1 输出:true

  • 示例 2: 输入:10 输出:false

  • 示例 3: 输入:16 输出:true

  • 示例 4: 输入:24 输出:false

  • 示例 5: 输入:46 输出:true

分析:

  • 数字需要拆分成数组
  • 数组进行全排列
  • 对所有全排列结果处理,首位为 0 的不处理
  • 将全排列结果转换成数字,并判断是否为 2 的幂数

如何判断 2 的幂数很简单,只要一直除以 2 最后等于 1,且过程中可以取 2 模 即可

var isSqrt = function (num) {
  let left = 0, right = num;
  while (num != 1) {
    if (num % 2 != 0) {
      return false;
    }
    num /= 2;
  }
  return true;
}

还有一种高效的方法: 一个数 n2 的幂,当且仅当 n 是正整数,并且 n 的二进制表示中仅包含 11。 因此我们可以考虑使用位运算,将 n 的二进制表示中最低位的那个 1 提取出来,再判断剩余的数值是否为 0 即可。下面介绍两种常见的与「二进制表示中最低位」相关的位运算技巧。

  1. 「临近按位与」
return n > 0 && (n & (n - 1)) === 0;

其中 & 表示按位与运算。该位运算技巧可以直接将 n 二进制表示的最低位 1 移除

  1. 「取反按位与」
return n > 0 && (n & -n) == n;

由于负数是按照补码规则在计算机中存储的,-n 的二进制表示为 n 的二进制表示的每一位取反再加上 1,再与 n 按位与运算,还等于 n



将数字拆分成数组也很简单,一直取余数即可 ```JavaScript let arr = []; let value = 0 while (n) { value = n % 10; arr.push(value); n = Math.floor(n / 10); } ```

代码如下:

/**
 * @param {number} n
 * @return {boolean}
 */
var reorderedPowerOf2 = function(n) {
  let arr = [];
  let value = 0
  while (n) {
    value = n % 10;
    arr.push(value);
    n = Math.floor(n / 10);
  }

  let allPermute = permuteUnique(arr);
  
  let ans = allPermute.some((item) => {
    item = item.join('');
    if (item[0] != '0') {
      item = Number(item);
      if (isSqrt(item)) {
        return true;
      }
    }
  })
  return ans || false;
};

var permuteUnique = function (nums) {
  let ans = [];
  backtrack(ans, nums, 0);
  return ans;
}

var backtrack = function (result, curArray, curIdx) {
  let leng = curArray.length;

  if (curIdx === leng) {
    result.push(curArray);
    return;
  }

  let sortArr = curArray.slice(curIdx);
  sortArr.sort((a, b) => a - b );
  curArray = curArray.slice(0, curIdx).concat(sortArr);

  for (let i = curIdx; i < leng; i++) {
    if (i < leng - 1 && curArray[i] === curArray[i + 1]) { continue; }
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
    backtrack(result, curArray.concat([]), curIdx + 1);
    [curArray[i], curArray[curIdx]] = [...swap(curArray[i], curArray[curIdx])];
  }
}

var swap = function (a, b) {
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;
  return [a, b];
}

var isSqrt = function (n) {
  return n > 0 && (n & (n - 1)) === 0;
}

都看到这里了,就点个赞吧