全排列算法(转)
列出全排列的初始思想:
解决一个算法问题,我比较习惯于从基本的想法做起,我们先回顾一下我们自己是如何写一组数的全排列的:1,3,5,9(为了方便,下面我都用数进行全排列而不是字符)。
1,3,5,9.(第一个)
首先保持第一个不变,对3,5,9进行全排列。
同样地,我们先保持3不变,对5,9进行全排列。
保持5不变,对9对进行全排列,由于9只有一个,它的排列只有一种:9。接下来5不能以5打头了,5,9相互交换,得到
1,3,9,5.
此时5,9的情况都写完了,不能以3打头了,得到
1,5,3,9
1,5,9,3
1,9,3,5
1,9,5,3
这样,我们就得到了1开头的所有排列,这是我们一般的排列数生成的过程。再接着是以3、5、9打头,得到全排列。这里还要注意的一点是,对于我们人而言,我们脑子里相当于是储存了一张表示原有数组的表,1,3,5,9,1开头的所有排列完成后,我们选择3开头,3选完了之后,我们选择5开头,而不会再返过来选1,而且知道选到9之后结束,但对于计算机而言,我们得到了3,5,1,9后,可能再次跳到1当中,因为原来数组的顺序它已经不知道了,这样便产生了错误。对于算法的设计,我们也可以维护这样一个数组,它保存了原始的数据,这是一种方法。同时我们还可以再每次交换后再交换回来,变回原来的数组,这样程序在遍历的时候便不会出错。读者可以练习一下这个过程,思考一下你是如何进行全排列的,当然,你的方法可能和我的不太一样。
我们把上面全排列的方法归纳一下,基本上就是:任意选一个数(一般从小到大或者从左到右)打头,对后面的n-1个数进行全排列。聪明的读者应该已经发现,这是一个递归的方法,因为要得到n-1个数的全排列,我们又要先去得到n-2个数的全排列,而出口是只有1个数的全排列,因为它只有1种,为它的本身。写成比较规范的流程:
1.开始for循环。
2.改变第一个元素为原始数组的第一个元素(什么都没做)。
3.求第2个元素到第n个元素的全排列。
4.要求第2个元素到第n个元素的全排列,要递归的求第3个元素到第n个元素的全排列。
......
5.直到递归到第n个元素到第n元素的全排列,递归出口。
6.将改变的数组变回。
7.改变第一个元素为原始数组的第二个元素。
(注:理论上来说第二次排列时才改变了第一个元素,即第6步应该此时才开始执行,但由于多执行一次无义的交换影响不大,而这样使得算法没有特殊情况,更容易读懂,如果一定要省时间可以把这步写在此处,这种算法我在下文中便不给出了,读者可以自己写。)
5.求第2个元素到第n个元素的全排列。
6.要求第2个元素到第n个元素的全排列,要递归的求第3个元素到第n个元素的全排列。
......
5.直到递归到第n个元素到第n元素的全排列,递归出口。
6.将改变的数组变回。
......
8.不断地改变第一个元素,直至n次使for循环中止。
为了实现上述过程,我们要先得到从第m个元素到第n个元素的排列的算法:
从第m个元素到第n个元素的全排列的算法:
public class Test { public static void main(String[] string) { int[] A = { 1, 2, 3 }; Full_Array(A, 3); } static void Full_Array(int A[], int n) { Permutation2(A, 0, n); } static void swap(int[] a, int m, int i) { int temp = a[m]; a[m] = a[i]; a[i] = temp; } static void Permutation2(int a[], int m, int n) { Set set = new HashSet(); if (m == n) { // Print(A); //直接输出,因为前n-1个数已经确定,递归到只有1个数。 System.out.println(a); return; } else { for (int i = m; i < n; i++) // 进入for循环,对应第一步 { swap(a, m, i); // 交换,对应第二步 Permutation(a, m + 1, n); // 递归调用,对应三至五步 // swap(a, m, i); //交换,对应第六步 } } } }