全排列-递归实现+字典序算法-java

全排列

输入长度为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 ] < A [ k + 1 ] ( k < n 1 ) 的元素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步可以从后向前搜索,找到满足条件的数,可以提高效率。

字典序使用迭代的方式,避免了递归对栈的大量调用和时间开销,生成下一个序列的时间复杂度为 O ( n )

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”,求离它最近且大于它的整数序列。
    1. 已知在字典序中,固定数字组成的序列,最小的序列是顺序序列“12345”,最大的序列是逆序序列“54321”;
    2. 为了和原序列“12354”接近,需要尽量保持高位不变,低位在最小的范围内变换顺序,那么从第几位开始变换?这取决于当前整数的逆序区域;
    3. “12354”的逆序区域是最后两位“54”,已经是最大值了,那么只能从前一位“3”处开始变换,从后面的逆序区域中寻找到大于3的最小数字,和3的位置进行互换:12354 ——> 12453;
    4. 倒数第3位已经确定,但最后两位仍然是逆序状态,我们需要把最后两位转变回顺序,以此保证在倒数第3位数值为4的情况下,后两位尽可能小:12453 ——> 12435;

获得字典序下一个序列的三个步骤:
1. 从后向前查看逆序区域,找到逆序区域的前一位;
2. 把逆序区域的前一位和逆序区域中大于它的最小数字交换位置;
3. 把原来的逆序范围转为顺序;

猜你喜欢

转载自blog.csdn.net/smj19920225/article/details/80239615
今日推荐