前端算法面试必刷题系列[22]

296 阅读3分钟

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

39. 排列序列 (permutation-sequence)

标签

  • DFS
  • 剪枝优化
  • 数学归纳
  • 困难

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:“123”,“132”,“213”,“231”,“312”,“321”,给定 n 和 k,返回第 k 个排列。 例如 n = 3, k = 3 时 返回 "213"

相关知识

全排列没有看过的同学请移步这篇热身,全排列

基本思路

我们先利用全排列算法,直接排出所有可能,选第k个就成。

写法实现

// 这个跟上面一篇文章一模一样的方法
var permute = function(nums) {
  let [res, len, usedSet] = [[], nums.length, {}]
  let dfs = (curPath) => {
    if (curPath.length === len) {
      res.push(curPath.slice())
      return
    }
    for (let i = 0; i < len; i++) {
      if (usedSet[i]) {
        continue;
      }
      curPath.push(i)
      usedSet[i] = true
      dfs(curPath)
      curPath.pop()
      usedSet[i] = false
    }
  }
  dfs([])
  return res
};

// 直接使用全排列结果取第k个
var getPermutation = function(n, k) {
  let originArr = new Array(n).fill(0).map((item, index) => index + 1)
  let permuteArr = permute(originArr)
  return permuteArr[k - 1].join('')
};

let n = 3, k = 3
console.log(getPermutation(n, k))

但我们在leetcode中执行这样的代码,会告诉你超时,因为这种直接暴力DFS,列出所有可能,效率太低了,我们稍微先改进下,用上 k,当到达k个就不往下了,省了一大波事。

var getPermutation = function(n, k) {
  let [count, usedSet] = [0, {}]
  let dfs = (curPath) => {
    if (curPath.length === n) {
      // 用一个count记录下第几个排列,到k个就直接return
      count++;
      if (count === k) {
        return curPath.join('')
      }
      return
    }
    for (let i = 1; i <= n; i++) {
      if (usedSet[i]) {
        continue;
      }
      curPath.push(i)
      usedSet[i] = true
      // 用变量存下res
      let res = dfs(curPath)
      curPath.pop()
      usedSet[i] = false
      // 如果存在res则直接返回,递归出口
      if (res) {
        return res
      }
    }
  }
  return dfs([])
};

let n = 3, k = 3
console.log(getPermutation(n, k))

这样就满足了吗,并不会,我们再思考,找些规律,让剪枝更高效

我们假设 n = 4, k = 15

第一轮我们首先定下第一位,则一共有4组,每组有6个数字,这个6其实就是 (4-1)!,也就是 剩余不确定的3个数的组合数 见下图

以1开头   以2开头   以3开头   以4开头 

1 234    2 134    3 124
1 243    2 143    3 142
1 324    2 314    3 214  。。。。。。
1 342    2 341    3 241
1 423    2 413    3 412
1 432    2 431    3 421
                第一轮k在这组
 6个      6个      6个       6个

判断k(15 > 6),不在第一组剪掉第一组6,剩下k(15 - 6 = 9),第二组k(9 > 6)剪掉第二组,k(9 - 6 = 3),那就在第三组,所以我们定下第一位,就是3 那么这个 k 我们可以判断在第 3 组内。

进行下一轮,递归的判断在哪组内,最后得出答案 我们再模拟下,下一轮 我们现在 k(3)124142214241412421中,注意第一轮用过了 3,进used数组

以1开头   以2开头   以4开头

1 24     2 14     4 12
1 42     2 41     4 21
                
 2个      2个      2个
       k=3在这组

依次下去,再来一轮,直到找到答案 3214

按思路编代码

const getPermutation = (n, k) => {
  let usedSet = {}
  let permuteCounts = 1;

  // 一共有permuteCounts组排列也就是 n!
  for (let i = 1; i <= n; i++) {
    permuteCounts = permuteCounts * i;
  }

  const permute = (curPath) => {
    const curLen = curPath.length;

    // 以当前的curPath来看,一个分组的数字个数,之后用k来比较
    permuteCounts = permuteCounts / (n - curLen);

    if (curLen == n) {
      return curPath.join('');
    }

    for (let i = 1; i <= n; i++) {
      if (usedSet[i]) {
        continue;
      }
      // 如果当前k大于一组的个数,说明当前不在这组内,直接减去一组的个数,并跳过本次循环的 i
      if (k > permuteCounts) {
        k = k - permuteCounts;
        continue;
      }
      curPath.push(i);
      usedSet[i] = true;
      // 本轮i确认好了,其实就是确认好了组,进入下一轮
      return permute(curPath);
    }
  };

  return permute([]);
};

let n = 4, k = 15
console.log(getPermutation(n, k))

其实很多优化可以用数学归纳法来解决。用数学思想会让你写出高效程序,一般工具类型的可以用这种高效的方式解决。写好注释也会有很好的可读性。

另外向大家着重推荐下这位大哥的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考