java aab全排列_关于全排列算法的思考

已经是求职过程中第三次碰到这个问题了,笔试两次,又面了一次,觉得这个问题确实值得深思啊。我还是尽量总结下吧,说不定以后还会碰到。

问题:已知输入为一个字符串,求其全排列的输出。比如输入为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为示例画了个调用图:

a4c26d1e5885305701be709a3d33442f.png

这个方法有两个问题:

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为例画个程序图吧

a4c26d1e5885305701be709a3d33442f.png

这个方法的一个巧妙之处在于,每次和子序列的首元素交换,然后调用递归并返回后,重新再进行一次交换,这样就能保证在递归的某一层中,每轮循环开始第一次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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值