参考链接
为了更好地理解,我们使用剑指offer
上的一道题目来开始:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。(输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。)
对于这种问题,我们很容易想到用枚举法,ok,let's start
。通过枚举这种方式,虽然这种方式很笨拙,但也很有效,可以帮助我们更好的理清思路。对于这个问题,需要分两种情况进行讨论:包含重复元素或者不包含重复元素。
不包含重复元素
给定字符串abcd
,对其进行全排列。
以a
开头为例
a
/ | \
b c d
/ \ /\ / \
/ \ / \ / \
c d b d b c
| | | | | |
d c d b c b
| | | | | |
----------------------------------
| abcd acdc acbd acdb adbc adcb|
----------------------------------
类似地,我们可以以b
,c
,d
打头。
根据上面的经验,我们很容易得出这样的思路,
- 对
a
、b
、c
、d
四个字母进行全排列时,先确定第1
位(从1
开始计数)。 - 对后边三位进行全排列,先确定第
2
位。 - 对后面两位进行全排列,先确定第
3
位。 - 到最后一位时,因为只剩下一个字母,对一个字母全排列只有一种情况,那就是自身。
根据上面的分析,很容易想到使用递归函数。
要使用递归,必须考虑两个问题:递归终止条件和问题规模的缩小。
递归终止条件
当只剩下,一个字母时,递归到底。
如何缩小问题规模?
根据前面的树图,通过不断确定前n
位,也就是固定前面n
位,保持不变。一开始,n=1
,也就是保持a
不变,对b
,c
,d
全排列;然后n=2
,保持前2
位不变,对后面两位进行全排列;接着n=3
,保持前3
位不变,对后面一位进行全排列。当n=4
时问题规模达到最小,问题解决。
伪代码
// arr即表示问题的输入,from 表示从哪里开始全排列。
function permutation(arr,from){
if(from===arr.length-1){ // 当from到达arr最后一个元素时,问题规模达到最小
//...do something here
}
// ...something need to do...here
// 缩小问题规模
permutation(arr,from+1)
}
我们现在做这样的一个假设,假设给定的一些序列中第一位都不相同,那么就可以认定说这些序列一定不是同一个序列,这是一个很显然的问题。有了上面的这一条结论,我们就可以同理得到如果在第一位相同,可是第二位不同,那么在这些序列中也一定都不是同一个序列。
那么,这个问题可以这样来看。对
T
中的第一个位置,那么对于剩下的序列可以看成是一个全新的序列
序列T1
可以认为是与之前的序列毫无关联了。同样的,我们可以对这个T1
进行与T
直到T
中只一个元素为止,这样我们就获得了所有的可能性。
示意图如下
通过上面的示意图,我们可以得到这样的思路,每一位的所有情况就是与其后面的元素进行交换位置。(因为位于前面位置的元素已经交换过一次,所以不需要考虑前面的元素。)
根据上面的分析,我们再来完善我们刚才写的代码:
// arr即表示问题的输入,from 表示从哪里开始全排列。
var ret = [] // 定义数组存储排列结果
function permutation(arr,from){
if(from===arr.length-1){ // 当from到达arr最后一个元素时,问题规模达到最小
ret.push(arr.join(''))
}
// ...something need to do...here
// 缩小问题规模
for(var i=from;i<arr.length;i++){
swap(arr,from,i)//每一位都与第一位发生交换,i===from时,也就是自己和自己交换,并不会发生值的变化,所以我们可以直接这样处理,不需要让i从from+1开始,当然也可以从i=from+1开始循环。
permutation(arr,from+1) // 固定位置后移
swap(arr,from,i)// 这一步的操作是为了在循环体中的第一步,我们对arr,进行了位置交换,对数组产生了影响。arr的顺序发生了变化,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下,所以每次交换后,要还原,确保初始状态一致。
}
}
// 位置交换
function swap(list, m, n) {
var temp = list[m];
list[m] = list[n];
list[n] = temp;
}
包含重复元素
以abca
为例进行分析
a | b | c
/ | \ | / \ | / \
b c a | a c | a b
/\ /\ /\ | /\ | | / \ |
c a a b b c | a c a | a b a
| | | | | | | | | | | | | |
a c b a c b | c a a | b a a
思路分析:
- 对
a
、b
、c
、a
四个字母进行全排列时,先确定第1
位(从1
开始计数)。第1
位有4
中情况,因为a
出现过一次,第二次出现时,结果与第一次全排列结果一样,所以省去。 - 对后边三位进行全排列,先确定第
2
位。同样判断是否出现相同元素,出现则跳过。 - 对后面两位进行全排列,先确定第
3
位。同样判断是否出现相同元素,出现则跳过。 - 到最后一位时,因为只剩下一个字母,对一个字母全排列只有一种情况,那就是自身。 对与包含重复元素与包含重复元素的思路极其类似,只是多了一步判断,也就是判断当前元素之前是不是开过头。
function permutation(arr,from){
if(from===arr.length-1){
ret.push(arr.join(''))
}
for(var i=from;i<arr.length;i++){
if(!hasAppeared(arr,from,i)){
swap(arr,from,i)// 这一步的操作是将元素i作为打头元素,也就是以元素i作为开头,在此之前,我们需要对i是否打过头进行判断。也就是看看再[from,i-1]之间看看arr[i]是否出现过。
permutation(arr,from+1)
swap(arr,from,i)
}
}
}
function hasAppeared(arr,from,to){
if(i>from){
for(var i=from;i<to;i++){
if(arr[i]===arr[to]){
return true
}
}
}
return false
}
解题代码
function Permutation(str) {
// write code here
if(str.length===0){
return []
}
var res = []
perm(str.split(''),0,str.length-1)
return res.sort();
function perm(arr, from, to) {
if (from === to) {
res.push(arr.join(''))
return;
}
for (var i = from; i <=to; i++) {
if (!isRepeat(arr, from, i)) {
swap(arr, from, i)
perm(arr, from + 1, to)
swap(arr, from, i)
}
}
}
function isRepeat(list, from, to) {
if (to > from) {
for (var i = from; i < to; i++) {
if (list[i] === list[to]) {
return true;
}
}
}
return false;
}
function swap(list, m, n) {
var temp = list[m];
list[m] = list[n];
list[n] = temp;
}
}