这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。
39. 排列序列 (permutation-sequence)
标签
- DFS
- 剪枝优化
- 数学归纳
- 困难
题目
这里不贴题了,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) 在 124、142、214、241、412、421中,注意第一轮用过了 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,我看到就通过,暗号对不上不加哈,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧