写在前面:本文中的代码是我没看算法书时,纯自己理解加调试出来的代码 T - T,有很多不成熟的地方,也没有很好地利用java语言的特性,请见谅
排列:从n个不同元素中任取m个元素,按照一定顺序排列。
全排列:当m=n时,所有排列情况为全排列。
1、递归算法的设计思路
a、算法的基本原理
以 abc 为例:求 abc 的全排列可看做求 a[bc]、b[ac]、c[ab] 全排列的过程,因此可以依照下面的函数声明写出式子:
doAnagram(abc,3) = doAnagram(abc,2) + doAnagram(bac,2) + doAnagram(cab,2)
ps:好像因为我的定义。。这个看起来不是很像递归式子
b、函数声明
/**
*tmp为要进行全排列的字符数组
*current为当前要进行排列的元素个数
**/
public void doAnagram(char[] tmp, int current);
c、递归的基准情形
我选择了current == 2 作为递归的基准情形,因为如果定为 current == 1的情况会使递归调用更深一层,使得所费时间更久(下文有两种基准情形所花费时间的对比)。
当current == 2时,即当字符串长度为2时,求其全排列十分简单,当两个字符不同时交换位置即可
以 ab 为例, ab 的全排列为 ab,ba;
以 aa 为例, aa 的全排列为 aa。
2、具体代码实现
Anagram.java
/**
* 递归全排列
* @author Yvonne
*
*/
public class Anagram {
private char[] str;//要进行全排列的字符串
private static int total = 0;//总共有多少种情况
public Anagram ( String str ){
this.str = str.toCharArray();
}
public void doAnagram(char[] tmp, int current ){
//输出全排列
int size = tmp.length;
if ( current == 2 ){
System.out.print((++total)+": ");
System.out.println(tmp);
if (tmp[size - 2] != tmp[size - 1]) {
//如果字符串的倒数两位是不同的,则交换,total值加1
swap(tmp, size - 2, size - 1);
System.out.print((++total) + ": ");
System.out.println(tmp);
}
return ;
}
//strTmp是用来存储一下当前current值下进行全排列的字符串
//例:doAnagram("abc",2)
//strTmp = "bc"
StringBuffer strTmp=new StringBuffer(size);
for( int i=size-current; i<size; i++){
strTmp.append(str[i]);
}
for ( int i=size-current; i<size; i++){
if( i == size - current ||
( str[ size - current ] != str [ i ] && (strTmp.indexOf(String.valueOf(str[i]))+size - current == i )) ){
//判断这次该交换到最前的字符是否跟上一次交换的字符相同
//判断这次交换的字符是否在之前交换过
//即,该字符,在被排列的这组字符串中是第一次出现,并且,
//没有被交换到上一层递归被排列字符串的首位,
//则交换到本次递归被排列的字符串的首位
//str[ size - current ]即在本次递归中不动的首位字符
swap(str , size - current, i);
if( ! str.equals(tmp)){
//tmp = String.valueOf(str).toCharArray();
//如果用上行这样转换的话,tmp会是一个新对象,导致
//递归过程中产生过多的对象,所以用下面的循环代替
for ( int i1=0; i1< size; i1++){
tmp[ i1 ] = str[ i1 ];
}
}
doAnagram(tmp, current - 1);
swap(str, size - current, i);
}
}
}
public static void swap(char[] tmp, int i, int j){
if(tmp[i]!=tmp[j]){
tmp[i] = (char) (tmp[i] + tmp[j]);
tmp[j] = (char) (tmp[i] - tmp[j]);
tmp[i] = (char) (tmp[i] - tmp[j]);
}
}
}
测试代码AnagramApp.java
public class AnagramApp {
public static void main(String[] args) {
// TODO Auto-generated method stub
String str = "abc";
Anagram ag = new Anagram(str);
char[] test = str.toCharArray();
long startTime = System.currentTimeMillis();
ag.doAnagram(test, str.length());
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime+"毫秒");
}
}
测试结果
测试用例:abc
1: abc
2: acb
3: bac
4: bca
5: cba
6: cab
0毫秒
//当出现重复字符时
测试用例:acc
1: acc
2: cac
3: cca
0毫秒
//当输入空字符串时
测试用例:
0毫秒
后更改了一下代码,测试了两种基准情形下的用时(更改的代码比较简单,这里不赘述),测试结果如下
//当基准情形为 current == 2 时
测试用例:123456789abc
共有479001600种情况
18595ms
//当基准情形为 current == 1 时
共有479001600种情况
46564ms
可以明显地看出,基准情形为 current == 2 时的耗时更短
3、改进与启发
由于在刚开始时,考虑问题太过于简单了,虽然一早就考虑到了有重复字符串的情况,但是一直没法正确解决,发现重复字符串的位置也对函数结果有不同的影响,最后在不得已的情况下选择了新建一个StringBuffer对象来存储当前需要进行交换的字符串。
(考虑原因:因为想偷懒用String的indexOf和lastIndexOf,又想尽可能的减少对象的创建,因为要把字符串的后几位存入字符串中,用String+=会不停地创建新对象。)
例如,测试用例:1212
只有在当前这个判断方法下才可以正确得出结果。
整体函数的编写不够简洁,感觉没有把java当java写,好像有点像把它当C语言在用QAQ。下回写非递归实现全排列的时候,一定把问题思考得更全面周到。
4、书上的代码及思路(下回更新)
5、全排列的非递归实现(下回更新)