已经是求职过程中第三次碰到这个问题了,笔试两次,又面了一次,觉得这个问题确实值得深思啊。我还是尽量总结下吧,说不定以后还会碰到。
问题:已知输入为一个字符串,求其全排列的输出。比如输入为abc,那么输出有以下几种:
abc
acb
bac
bca
cab
cba
即如果输入字符串的长度为N的话,会输出N!个结果。
方法一:递归
思路是这样的:我们维护两个序列,一个序列是要进行全排列的序列,我们暂称之为源序列,另一个序列是全排列之后的结果序列,我们称其为结果序列。过程如下:
1)初始时源序列为输入的字符串序列,结果序列为空
2)如果源序列中的元素个数大于1,则对源序列中的每一个元素,进行如下操作:
I. 以结果序列+该元素生成新的结果序列
II.
将该元素从源序列中剔除并保持其他元素顺序不变生成新的源序列
然后以I产生的结果序列和II产生的源序列为基础递归2)过程
3)如果源序列中元素个数不大于1,则打印结果序列+源序列
下面给出了该思路的java实现,参考[1]
public class MainClass {
public static void main(String args[]) {
permuteString("", "String");
}
public static void permuteString(String
beginningString, String endingString) {
if
(endingString.length() <= 1)
System.out.println(beginningString + endingString);
else
for (int i = 0; i < endingString.length(); i++)
{
try {
String newString = endingString.substring(0, i) +
endingString.substring(i + 1);
permuteString(beginningString + endingString.charAt(i),
newString);
} catch (StringIndexOutOfBoundsException exception) {
exception.printStackTrace();
}
}
}
}
这个实现中endingString对应叙述中的源序列,beginningString对应结果序列,为了方便理解,我以abc为示例画了个调用图:
这个方法有两个问题:
1)每次递归时都要产生新源序列和结果序列,作为递归参数传进去,上面的实现使用java语言写的,要是用C++的话就得用传值的方式生成std::string,不断地产生和释放新串,当输入串只有几个十几个字符时当然不是什么问题了,如果字符串长度一长,估计就扛不住了。
2)同样,当字符串长度一长,递归深度成线性增加,这个也可能照成性能上的影响。
这个其实在百度招聘的笔试试卷上见过一次,我当时也没多想,直接给出了本文第二种方法的实现代码,结果面试时面试官说了一种比较容易懂的方法就是这种方法。我跟面试官解释了我的代码半天,好像也没解释清楚。Sigh!
方法二:递归+置换
我先把实现代码给出来吧:
void Swap(int *a, int *b)
{
int tmp =
*a;
*a =
*b;
*b =
tmp;
}
int Permutation(int A[], int start, int end)
{
int n =
0;
if (start
>= end) //结束条件
{
for (int i = 0; i <= end; ++i)
{
printf("%d ", A[i]);
}
printf("\n");
n++;
}
else
{
for (int i = start; i <= end; ++i)
{
Swap(&A[start], &A[i]);
n += Permutation(A, start + 1, end);
Swap(&A[start], &A[i]);
}
}
return
n;
}
int main(int argc, char *argv[])
{
int A[] =
{1, 2, 3, 4};
int num =
Permutation(A, 0, 3);
return
0;
}
有点区别的是我这里用的是整数数组,不过我想和字符串数组应该是一个道理。那么怎么解释这种方法呢?两次面试都是拿着代码找解释,那个尴尬啊,这也更加坚定了我要彻底搞清楚这个问题的决心。
其核心是:将每个元素放到n个元素组成的队列最前方,然后对剩余元素进行全排列,依次递归下去。[2]这样解释起来好像还是有点不明白,我还是以abc为例画个程序图吧
这个方法的一个巧妙之处在于,每次和子序列的首元素交换,然后调用递归并返回后,重新再进行一次交换,这样就能保证在递归的某一层中,每轮循环开始第一次Swap前序列顺序与子递归返回第二次Swap后的顺序保持一致,即图中的竖直方向。这样就使得子调用递归后不会影响下一轮循环的调用。
这种方法不需要额外创建字符串数组来保存中间信息,字符串序列也是通过引用的方式传入,因此不存在上一种方法中的第一个问题,但是由于还是使用了递归,所以当字符串过长时,还是会出现递归层次过深的问题。
关于全排列这个问题,还有很类似的有趣的问题,就是部分排列,比如今年大摩就有这么一道笔试题:0,2,3,。。。9这十个数,从中选出4个数进行排列,打印所有可能的排列。比如:
1 2 3 4
1 2 3 5
。。。
9 8 7 6
其实就是全排列的变种,全排列是求Ann,而大摩的那道题是要求部分排列Anm,m
<
n,其实上面两种方法都有对应的解法,第一种只要把源序列长度结束判断条件改为endingString.length()
<= (n – m) 即可;第二种,将结束条件改为start + (n - m)
>= end即可。
笔试完大摩,我同学说这个题好简单,写四个循环然后加些不等的比较判断就搞定了。嗯,确实,如果仅仅就那个题来说,确实可以,但是如果不是选4个数,而是选6个,8个呢?一旦n和m变大,这种直接写循环的方法就行不通了,因此我们必须考虑如何去写这个循环,使得代码不依赖m和n,这也是EMC面试官给我出的一个难题,很遗憾,我没有当场做出来,只能回来考虑了。
方法三:非递归循环
估计这个方法就是面试官想要的方法了,还记得他一直在问,如何动态控制循环层数,如何判断循环结束等等问题,我也是模模糊糊地回答了下,其实根本就不知道,呵呵。回来考虑了很久,还是没头绪,直到看了[3],才明白到底是怎么回事,哎,我的资质确实不怎么样。还是先看看别人的算法流程吧:
1、用一个数组 repeat_cnt[] 来保存每层的循环变量:repeat_cnt[0] 保存第 0
层循环变量的位置,repeat_cnt[1] 保存第 1 层循环变量的位置......repeat_cnt[n-1] 保存第 n-1
层循环变量的位置
2、标记当前正在第几层循环:layer
3、集合长度已知 :n = strlen(s)
4、临时数组:tmp_seq[],长度为 n,用于存储一个可能全排列的序列
3、算法描述如下:
循环体(layer == 0 且 repeat_cnt[0]== size 则退出循环)
{
如果(前 n-1 层)
{
取出该层循环变量:pos=repeat_cnt[layer]
如果 (pos 到达该层末尾,即 pos==size)
{
repeat[layer] = 0//该层循环变量归零
layer--//回到上一层
continue
}
否则
{
tmp_seq[layer] = s[pos]
repeat_count[layer]++ //该层循环变量增加1
layer++//层数增加 1
continue
}
否则(在最内层)
{
不停改变 tmp_seq 最后一个元素,每改变一次,就得到一种新的组合,该层循环变量增加1
当最内层也到达 s 末尾:将该层循环变量归零,回到上层
}
}
下面是将原作者用Python编写的 permutation
函数改成了C++版本的,对于C/C++程序员来说肯能更直接一些,该函数接受两个参数
第一个参数:要全排列的字符串
第二个参数:选几个数排列,默认全排序
第三个参数:是否允许重复,默认不允许
void permutation(char* s, int m = 0, bool duplicate =
false)
{
int n =
strlen(s);
if(n
< 1 || m < 0 || m >
n)
return;
if(m ==
0) //默认全排列
m = n;
int
*repeat_cnt = new int[n](); //循环计数器,一层一个,共 n 个,都初始化为 0
char
*tmp_seq = new char[n](); //存放排列结果序列的空间
int layer =
0; //当前正在循环层
int
pos; //本循环层的层计数器
while(true)
{
if(layer == 0 && repeat_cnt[0]
>= n) //当前在第 0 层,且第 0 层计数器达到末尾时退出循环
break;
if(layer < m -
1) //小于 n-1 层
{
pos = repeat_cnt[layer];
if(pos <
n) {
//层计数器 < n,保存层计数器指向的元素,计数器前进一位,增加一层
if(!duplicate)
{
//不允许重复
bool found = false;
for(int i = 0; i < n; ++i)
{
if(pos + 1 == repeat_cnt[i])
{
found = true;
break;
}
}
if(found)
{
//检查到重复,计数器前进一位,继续
repeat_cnt[layer] = repeat_cnt[layer] + 1;
continue;
}
}
tmp_seq[layer] = s[pos];
repeat_cnt[layer] = repeat_cnt[layer] + 1;
layer++;
}
else
{
//否则,层计数器归零,向上一层
repeat_cnt[layer] = 0;
layer--;
}
}
else
{
//第 m - 1 层:计数器每移动一个位置,都是一种组合
pos = repeat_cnt[layer];
if(pos < n)
{
if(!duplicate)
{
//不允许重复
bool found = false;
for(int i = 0; i < n; ++i)
{
if(pos + 1 == repeat_cnt[i])
{
found = true;
break;
}
}
if(found)