写在前面:本文中的代码是我没看算法书时,纯自己理解加调试出来的代码 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、全排列的非递归实现(下回更新)