[算法]字符全排列
今天介绍一下字符全排列的算法。
问题
给出(没有重复的)字符数组,求出所有由这些字符组合的排列,每个字符使用且仅使用一次。例如:
getAll(['a', 'b', 'c']);
// ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
递归
首先讲一下递归的想法:
- 当字符数组长度为1时,输出该字符
- 当字符数组长度大于1时,取其首字母,求出长度-1的串的全排列,将首字母插入每一个排列的任意位置
因为今天要讲的不是这样的做法,具体代码就不贴了。
我们不难发现这里解空间的规模是n!
。以上算法会需要多个((n-1)!
)解同时存在于内存中,因此空间复杂度非常高。
下一个最小的数
因此我们需要一个空间复杂度较低的算法,即我们需要可以『逐一』求出所有解的方法。
让我们将字母替换为数字来考虑这个问题:
getAll([1, 2, 3]);
// [123, 132, 213, 231, 312, 321]
在所有结果中,最小的值为123
,即所有数字顺序排列,最大的值为321
,即所有数字逆序排列(如果输入个数大于10,我们只要用n
进制来想象问题就可以了)。
那接下来我们考虑这样一个问题,给出解中的任意一个,怎么求出恰好比它大的一个解?
我们想象数字4 3 7 5 1
,比它恰好大的数字数4 5 1 3 7
。我们从右开始寻找第一组相邻的正序的数字,即3 7
,然后把3
替换为其右侧仅比它大的数字5
。然后把剩余的数字1 7
以及3
一起正序排序(即1 3 7
数字的最小组合),之后连在一起,4
+ 5
+ 1 3 7
。
于是我们可以从123
开始,逐一求下一个最小的数,直至达到最大数321
。
- 为什么要从右向左开始搜索?因为我们要求的是最接近的一个解,因此应当尽量保持高位的数字不变。
- 为什么要寻找正序的数字对?因为连续逆序的数字必然是最大的,例如
43751
中的后三位,751
,是1、5、7
这三个数字所能组成的最大值。
然后我们就可以实现这个算法,实现逐一求解,从而避免内存的问题:
const factorial = function factorial (num) {
let rtn = 1;
for (let i = 2; i <= num; i++) {
rtn *= i;
}
return rtn;
};
const getAll = function getAll(list) {
let count = 0;
let next = list.slice();
while (next) {
count++;
console.log(next.join(''));
next = getNext(next);
}
console.log(count, factorial(list.length));
};
const getNext = function getNext(list) {
const rest = [];
for (let i = list.length - 1; i > 0; i--) {
const pre = list[i - 1];
const next = list[i];
rest.push(next);
if (pre < next) {
const head = list.slice(0, i - 1);
const max = list.length + 1;
for (let j = pre; j < max; j++) {
let index = rest.indexOf(j);
if (index !== -1) {
rest.splice(index, 1);
rest.push(pre);
head.push(j);
return head.concat(rest.sort((a, b) => a - b))
}
}
}
}
return null;
};