全排列
输入长度为n的元素各异的数组,各个元素按照一定顺序排列起来,组成的所有序列集合就是全排列,比如输入[1,2,3],全排列为:”[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]”。
- 常用的实现方法有递归法和字典序算法。
1.全排列的递归实现
给定一个n个元素数组,其全排列的递归过程可以描述如下:
1. 任意取一个元素放在第一个位置;
2. 对剩下的n-1个元素进行任意组合,可以看作是对长度为n-1的数组的全排列;
3. 重复2步,直到最后只剩下一个元素;
4. 重复1,2,3步,直到所有元素都在第一个位置放置过,全排列结束。
以数组[1,2,3]为例,其全排列的过程如下:
1. 取1放在第一个位置,后面连接着(2,3)的全排列;
- (2,3)的全排列,取2放在第一位,只剩下3 — [1,2,3]
- (2,3)的全排列,取3放在第一位,只剩下2 — [1,3,2]
2. 取2放在第一个位置,后面连接着(1,3)的全排列;
- (1,3)的全排列,取1放在第一位,只剩下3 — [2,1,3]
- (1,3)的全排列,取3放在第一位,只剩下1 — [2,3,1]
3. 取3放在第一个位置,后面连接着(1,2)的全排列;
- (1,2)的全排列,取1放在第一位,只剩下2 — [3,1,2]
- (1,2)的全排列,取2放在第一位,只剩下1 — [3,2,1]
public class perms{
public static void main(String[] args){
int[] nums = {1,2,3};
Permutation1(nums, 0, nums.length-1);
}
//递归全排列函数
public static void Permutation1(int[] nums, int low, int high){
if(low == high){
for(int num : nums){
System.out.print(num + " ");
}
System.out.println();
}
//在数组nums[low,high]中,依次把把下标为low,low+1,....,high的数值放在第一位
for(int i = low; i <= high; i++){
// 把下标为i的数,和下标为low的数进行交换
swap(nums, low, i);
// 对剩下的部分继续使用递归排列
Permutation1(nums, low+1, high);
// 把下标为i的数和下标为low的数交换回来,用以i+1的交换
swap(nums, low, i);
}
}
private static void swap(int[] nums, int idx1, int idx2){
int tmp = nums[idx1];
nums[idx1] = nums[idx2];
nums[idx2] = tmp;
}
}
由于递归将问题分解,相对比较容易理解,但是需要消耗大量的栈空间,如果函数栈空间不够,会发生溢出,而且函数调用开销较大。
2.全排列的字典序算法
字典序 是对集合A的元素所形成的序列,进行比较大小的一种方式;比较的方法是从前到后依次比较两个序列的对应元素,如果当前位置对应元素相同,则继续比较下一个位置,直到第一个元素不同的位置为止,元素值大的元素在字典序中就大于元素值小的元素(元素大的排在后面)。
- 以{1,2,3,4}为例,形成的排列 1234 < 1243,1234排在1243前面
字典序生成全排列的基本过程,
给定数组A[N],那么使用字典序输出全排列的方法基本过程描述如下:
1. 将A按元素大小递增排序,形成字典序最小的排列A1;
2. 左起从A[0]开始寻找最后一个满足
的元素A[k],n为元素个数;
3. 在A[k+1,n-1]中找到大于A[k]的最小数A[i],交换A[k]与A[i];
4. 反转A[k+1,n-1]之间的数据顺序,A[k+1]与A[n-1]交换,A[k+2]与a[n-2]交换……,这样就得到了A1在字典序中的下一个排列A2;
5. 重复步骤2~4,直到A的所有元素按照从大到小逆序排列,全排列完成。
字典序生成全排列的就是:先生成在字典序中的第一个序列,然后不断生成下一个序列。
- 在实际操作中,第2步和第3步可以从后向前搜索,找到满足条件的数,可以提高效率。
字典序使用迭代的方式,避免了递归对栈的大量调用和时间开销,生成下一个序列的时间复杂度为
public class perms{
public static void main(String[] args){
int[] nums = {1,2,3,4};
Permutation2(nums);
}
//递归全排列函数
public static void Permutation2(int[] nums){
//先把数组升序排列,作为字典序的第一个序列,并打印出来
Arrays.sort(nums);
printNums(nums);
// 不断寻找当前序列的下一个序列
while(true){
//1.寻找A[k],从后向前
int k = nums.length - 2;
while(k >= 0 && nums[k] >= nums[k+1] ){ k--; }
//如果k等于-1,说明整个序列都是逆序的,是字典序的最后一个序列了
if(k == -1){ return; }
//2.在A[k+1,n-1]中找到大于A[k]的最小数A[i],交换A[k]与A[i]
if(k >= 0){
int j = nums.length-1;
while(j>= k+1 && nums[j] <= nums[k]){ j--; }
swap(nums, k, j);
}
//3.反转A[k+1,n-1]之间的数据顺序
reverse(nums, k+1);
printNums(nums);
}
}
private static void swap(int[] nums, int idx1, int idx2){
int tmp = nums[idx1];
nums[idx1] = nums[idx2];
nums[idx2] = tmp;
}
private static void reverse(int[] nums, int idx){
int end = nums.length-1;
while(idx < end){
swap(nums, idx, end);
idx++;
end--;
}
}
private static void printNums(int[] nums){
for(int num : nums){
System.out.print(num + " ");
}
System.out.println();
}
}
需要注意的是:递归法求出的序列集合和字典序法求出的集合是不一样的,前者并没有按照大小排序。
全排列字典序算法的衍生问题
由全排序衍生出的一个常见问题是:输入一个序列,输出它在字典序中的下一个序列(LeetCode-31)
代码解答
- 这个问题也可以这样问:给定一个整数序列,如何找到离它最近且大于它的整数序列?下面通过这个案例,形象的解释一下字典序算法。
- 问题:给定序列为“12354”,求离它最近且大于它的整数序列。
- 已知在字典序中,固定数字组成的序列,最小的序列是顺序序列“12345”,最大的序列是逆序序列“54321”;
- 为了和原序列“12354”接近,需要尽量保持高位不变,低位在最小的范围内变换顺序,那么从第几位开始变换?这取决于当前整数的逆序区域;
- “12354”的逆序区域是最后两位“54”,已经是最大值了,那么只能从前一位“3”处开始变换,从后面的逆序区域中寻找到大于3的最小数字,和3的位置进行互换:12354 ——> 12453;
- 倒数第3位已经确定,但最后两位仍然是逆序状态,我们需要把最后两位转变回顺序,以此保证在倒数第3位数值为4的情况下,后两位尽可能小:12453 ——> 12435;
获得字典序下一个序列的三个步骤:
1. 从后向前查看逆序区域,找到逆序区域的前一位;
2. 把逆序区域的前一位和逆序区域中大于它的最小数字交换位置;
3. 把原来的逆序范围转为顺序;