一.全排列的递归实现
为方便起见,用123来示例下。123的全排列有123、132、213、231、312、321这六种。首先考虑213和321这二个数是如何得出的。显然这二个都是123中的1与后面两数交换得到的。然后可以将123的第二个数和每三个数交换得到132。同理可以根据213和321来得231和312。因此可以知道——全排列就是从第一个数字起每个数分别与它后面的数字交换。找到这个规律后,递归的代码就很容易写出来了:
public class Quanpailie { public static int arr[] = new int[] { 1, 2, 3,4 }; public static void main(String[] args) { prim(arr, 0, arr.length - 1); } private static void prim(int arr[], int begin, int end) { if (begin == end) { for (int i = 0; i <= end; i++) { System.out.print(arr[i] + " "); } System.out.println(); return; } else { for (int j = begin; j <= end; j++) { swap(arr, begin, j); // for循环将begin~end中的每个数放到begin位置中去 prim(arr, begin + 1, end); // 假设begin位置确定,那么对begin+1~end中的数继续递归 swap(arr, begin, j); // 换过去后再还原 } } } private static void swap(int[] arr, int n, int m) { int tmp = arr[n]; arr[n] = arr[m]; arr[m] = tmp; } }
注意这样的方法没有考虑到重复数字。因此现在要想办法来去掉重复的数列。
二.去掉重复的全排列的递归实现
由于全排列就是从第一个数字起每个数分别与它后面的数字交换。我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。用编程的话描述就是第i个数与第j个数交换时,要求[i,j)中没有与第j个数相等的数。下面给出完整代码:
public class Main { public static int arr[] = new int[] { 1, 2, 2,4}; private static int n=arr.length; private static boolean isSwap(int begin,int end){ for (int i = begin; i <end; i++) { if(arr[i]==arr[end]) return false; } return true; } private static void swap(int n, int m) { int tmp = arr[n]; arr[n] = arr[m]; arr[m] = tmp; } public static void prim(int begin,int end){ if(begin==end){ for (int i = 0; i <=end; i++) { System.out.print(arr[i]); } System.out.println(); }else{ for (int i = begin; i <=end; i++) { if(isSwap(begin, i)){ swap(begin, i); prim(begin+1, end); swap(begin, i); } } } } public static void main(String[] args) { prim(0, n-1); } }
三.全排列的非递归实现
要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。
如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。对于像"4321"这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。下面列出完整代码:
public class Main { public static int arr[] = new int[] { 1, 2, 2,4}; private static int n=arr.length; private static boolean isSwap(int begin,int end){ for (int i = begin; i <end; i++) { if(arr[i]==arr[end]) return false; } return true; } private static void swap(int n, int m) { int tmp = arr[n]; arr[n] = arr[m]; arr[m] = tmp; } public static void prim(int begin,int end){ if(begin==end){ for (int i = 0; i <=end; i++) { System.out.print(arr[i]); } System.out.println(); }else{ for (int i = begin; i <=end; i++) { if(isSwap(begin, i)){ swap(begin, i); prim(begin+1, end); swap(begin, i); } } } } public static void main(String[] args) { prim(0, n-1); } }
至此我们已经运用了递归与非递归的方法解决了全排列问题,总结一下就是:
- 全排列就是从第一个数字起每个数分别与它后面的数字交换。
- 去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。
- 全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。