全排列算法

1.全排列的定义和公式:

从n个数中选取m(m<=n)个数按照一定的顺序进行排成一个列,叫作从n个元素中取m个元素的一个排列。由排列的定义,显然不同的顺序是一个不同的排列。从n个元素中取m个元素的所有排列的个数,称为排列数。从n个元素取出n个元素的一个排列,称为一个全排列。全排列的排列数公式为n!,通过乘法原理可以得到。

2.时间复杂度:

n个数(字符、对象)的全排列一共有n!种,所以全排列算法至少时间O(n!)O(n!)的。如果要对全排列进行输出,那么输出的时间要O(nn!)O(n∗n!),因为每一个排列都有n个数据。所以实际上,全排列算法对大型的数据是无法处理的,而一般情况下也不会要求我们去遍历一个大型数据的全排列。

3.列出全排列的初始思想:

解决一个算法问题,我比较习惯于从基本的想法做起,我们先回顾一下我们自己是如何写一组数的全排列的:1,3,5,9(为了方便,下面我都用数进行全排列而不是字符)。 
【1,3,5,9】(第一个) 
首先保持第一个不变,对【3,5,9】进行全排列。 
同样地,我们先保持3不变,对【5,9】进行全排列。 
保持5不变,对9对进行全排列,由于9只有一个,它的排列只有一种:9。 
故排列为【1,3,5,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打头,得到全排列。

我们现在做这样的一个假设,假设给定的一些序列中第一位都不相同,那么就可以认定说这些序列一定不是同一个序列,这是一个很显然的问题。有了上面的这一条结论,我们就可以同理得到如果在第一位相同,可是第二位不同,那么在这些序列中也一定都不是同一个序列。 
那么,这个问题可以这样来看。对 
T=T=【x1,x1,x2,x3,x4,x5,........xn1,xnx2,x3,x4,x5,........xn−1,xn】 
我们获得了在第一个位置上的所有情况之后(注:是所有的情况),对每一种情况,抽去序列TT中的第一个位置,那么对于剩下的序列可以看成是一个全新的序列 
T1=x2,x3,x4,x5,........xn1,xnT1=【x2,x3,x4,x5,........xn−1,xn】 
序列T1T1可以认为是与之前的序列毫无关联了。同样的,我们可以对这个T1T1进行与TT相同的操作,直到TT中只一个元素为止。这样我们就获得了所有的可能性。所以很显然,这是一个递归算法。 
 第一位的所有情况:无非是将x1x1与后面的所有数x2,x3,.......xnx2,x3,.......xn

依次都交换一

所有元素均无相同的情况

基于上面的分析,我们知道这个可以采用递归式实现,实现代码如下:

private static void core(int[] array) {
        int length = array.length;
        fullArray(array, 0, length - 1);
    }

    private static void fullArray(int[] array, int cursor, int end) {
        if (cursor == end) {
            System.out.println(Arrays.toString(array));
        } else {
            for (int i = cursor; i <= end; i++) {
                ArrayUtils.swap(array, cursor, i);
                fullArray(array, cursor + 1, end);
            }
        }
    }

运行结果

[1, 2, 3]
[1, 3, 2]
[3, 1, 2]
[3, 2, 1]
[1, 2, 3]
[1, 3, 2]

这个答案就有一些让人匪夷所思了,为什么会有几组是重复的?为什么第一位里面没有 2? 
理论上,上面的代码没有问题,因为当我们循环遍历序列中每一位时,都有继续进行后面序列的递归操作。core()方法当然没什么问题,问题是出在fullArray()方法上了。很容易锁定在了那个for循环里。我们来仔细推敲一下循环体里的代码,当我们对序列进行交换之后,就将交换后的序列除去第一个元素放入到下一次递归中去了,递归完成了再进行下一次循环。这是某一次循环程序所做的工作,这里有一个问题,那就是在进入到下一次循环时,序列是被改变了。可是,如果我们要假定第一位的所有可能性的话,那么,就必须是在建立在这些序列的初始状态一致的情况下(感兴趣的你可以想想这是为什么)。 
好了,这样一来问题找到了,我们需要保证序列进入下一次循环时状态的一致性。而保证的方式就是对序列进行还原操作。我们修改fullArray()如下:

private static void fullArray(int[] array, int cursor, int end) {
        if (cursor == end) {
            System.out.println(Arrays.toString(array));
        } else {
            for (int i = cursor; i <= end; i++) {
                ArrayUtils.swap(array, cursor, i);
                fullArray(array, cursor + 1, end);
                ArrayUtils.swap(array, cursor, i); // 用于对之前交换过的数据进行还原
            }
        }
    }

修改后的运行结果

[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 2, 1]
[3, 1, 2]

存在相同元素的情况

上面的程序乍一看没有任何问题了。可是,如果我们对序列进行一下修改 array = {1, 2, 2}.我们看看运行的结果会怎么样。

[1, 2, 2]
[1, 2, 2]
[2, 1, 2]
[2, 2, 1]
[2, 2, 1]
[2, 1, 2]

这里出现了好多的重复。重复的原因当然是因为我们列举了所有位置上的可能性,而没有太多地关注其真实的数值。 
现在,我们这样来思考一下,如果有一个序列T = {a1, a2, a3, …, ai, … , aj, … , an}。其中,a[i] = a[j]。那么是不是就可以说,在a[i]上,只要进行一次交换就可以了,a[j]可以直接忽略不计了。好了,基于这样一个思路,我们对程序进行一些改进。我们每一次交换递归之前对元素进行检查,如果这个元素在后面还存在数值相同的元素,那么我们就可以跳过进行下一次循环递归(当然你也可以反着来检查某个元素之前是不是相同的元素)。 
基于这个思路,不难写出改进的代码。如下:

private static void core(int[] array) {
        int length = array.length;
        fullArray(array, 0, length - 1);
    }

    private static boolean swapAccepted(int[] array, int start, int end) {
        for (int i = start; i < end; i++) {
            if (array[i] == array[end]) {
                return false;
            }
        }
        return true;
    }

    private static void fullArray(int[] array, int cursor, int end) {
        if (cursor == end) {
            System.out.println(Arrays.toString(array));
        } else {
            for (int i = cursor; i <= end; i++) {
                if (!swapAccepted(array, cursor, i)) {
                    continue;
                }
                ArrayUtils.swap(array, cursor, i);
                fullArray(array, cursor + 1, end);
                ArrayUtils.swap(array, cursor, i); // 用于对之前交换过的数据进行还原
            }
        }
    }

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值