全排列算法的JS实现
----------------20210925 更新------------------
不知不觉离写下这篇博文已经过去了五六年,记得当时的我还没找到工作,一个人在出租屋里写着这些乱七八糟的东西。时光飞逝啊。
其实全排列是一个非常基础的问题,leetcode 上相关的题目也不少。通常来说可以用回溯算法解决。这里就再给出个简单的实现,掩盖一下当初的黑历史(bushi
function permutate(str) { const result = []; const used = Array.from({ length: str.length }, () => false); function backtracking(candidate, memo) { if (memo.length === str.length) { result.push(memo.slice()); return; } for (let i = 0; i < candidate.length; i++) { if (used[i]) continue; memo.push(candidate[i]); used[i] = true; backtracking(candidate, memo); used[i] = false; memo.pop(candidate[i]); } } backtracking(str, []); return result; }
-----------------原文---------------------
问题描述:给定一个字符串,输出该字符串所有排列的可能。如输入“abc”,输出“abc,acb,bca,bac,cab,cba”。
虽然原理很简单,然而我还是折腾了好一会才实现这个算法……这里主要记录的是解决问题中的思路。
我实现的是最普通的递归算法,也没有除重,嗯非递归及除重的算法以后再补上吧。
实现过程
首先明确函数的输入和输出,输入是一个字符串,输出么对于JS而言用数组来表示最恰当了,所以函数的雏形应该是这样的:
function permutate(str) { var result = []; return result; }
然后,确定是用递归的形式解决。递归的解法么,其实就是数学归纳法的寻找规律那一步。数学归纳法是什么样来着:第一步,给出基础值,比如输入为1的时候输出应该是成立的。第二步,假设对于输入n成立,证明输入n+1时也成立。
好了,所以先来完成第一步。对这个问题而言,基础情况应该是输入字符串为单个字符时的情况。这个时候输出应该是什么呢。当然是输入本身。但是,不要忘了输出应该是数组形式,所以接下来的样子:
function permutate(str) { var result = []; if(str.length > 1) {
} else if (str.length == 1) { return [str]; } return result; }
中间用了else if 而没有用else的原因是不清楚到最后是否要处理空字符串的情况,所以先留着个else。逻辑上应该位于第一个if里的return语句,放到了最后,比较清晰。
接着进行第二步,假设我们已经知道了n-1的输出,要由这个输出得出n的输出。在这个问题里,n-1的输入,对应着长度比当前输入的字符串少1的输入字符串。也就是说,如果我已经知道了“abc”的全排列输出的集合,现在再给你一个“d”,要怎样得出新的全排列呢?
很简单,只要对于集合中每一个元素,把d插入到任意相邻字母之间(或者头部和尾部),就可以得到一个新的排列。例如对于元素“acb”,插入到第一个位置,即可得到“dacb”,插入其余位置,可得到“adcb”,“acdb”,“acbd”。容易证明这样形成的新元素不会有重复。
在这里,对于每一个输入的str,我们把它分为两部分,第一部分为字符串的第一个字母,定义为left,第二部分为剩余的字符串,定义为rest,根据以上的假设,现在可以把 permutate(rest) 作为一个已知量看待。
function permutate(str) { var result = []; if(str.length > 1) { var left = str[0]; var rest = str.slice(1, str.length); var preResult = permutate(rest); /* Do some operation */ } else if (str.length == 1) { return [str]; } return result; }
接着对permutate(rest)里的每一个排列进行处理,将left插入到每一个位置中,每得到一个排列,便将它push到result里面去。
...... for(var i=0; i<preResult.length; i++) { for(var j=0; j<preResult[i].length; j++) { var tmp = preResult[i],slice(0, j) + left + preResult[i].slice(j, preResult[i].length); result.push(tmp); } } ......
有了字符串自带的slice方法省了不少事。一开始想到插入字符串想到的是splice方法,然而这个方法会对原始字符串进行修改,要是用它的话会出很无奈的bug……
到这里就告一段落了,把上面的片段插入到前面的注释的位置就是完整代码了。
然后这个函数对于空字符串的输入会输出空字符串,所以前面else if的if也可以去掉。
另一个问题
说起全排列我还想到了另一个问题:
给定一个数字字符串,输出组成这个字符串的每个数字的所有组合中,比当前数字大的下一个数字。
举例:对于输入“113”,它的所有不重复的排列组合为“113”,“131”,“311”,那么其中比当前数字大的下一个数字为“131”,所以输出为“131”。
我想到的第一个解法,首先求出当前字符串的所有排列组合,然后对返回的结果排序,再求出当前数字所在下标的下一个。
程序的样子差不多应该是这样:
function permutate() { //...... return result; } function main(str) { var all = permutate().sort(); var targetIndex = all.indexOf(str)+1; return result[targetIndex]; }
思路非常直接,然而这个算法的缺点也是很明显的:复杂度太高了。对于输入长度为 n 的字符串,光是排列算法就得计算 n!次。
所以这个想法可以pass。
实际上比较正确的算法是可以在O(n)的复杂度内求出结果的。不过在这里就不详细说明了。各种解决途径可以点击以下链接查看:
链接 (注:注册了codewars账号并给出一种解法后方可查看对应的solution)
之所以提到这个问题,是因为虽然不推荐使用全排列的方法解决这个问题,但是我们可以通过这个问题的解法,反过来给出全排列的一种非递归式解法。
例如我们要给出字符串“aabccdde"的全排列,可以把对应字母替换为“11233445”,然后调用上面问题的解法,依次输出每一个排列即可。因为本身这个算法的复杂度很低,所以不会影响到最终全排列算法的复杂度。
缺点么,如果有十个以上的不同字符,那就没有办法了……