全排列算法的全面解析

概述

对数组进行全排列是一个比较常见问题,如果是一个比较喜欢考算法的公司(貌似一些大公司都比较喜欢考算法),那么估计就会考察应聘者这个全排列的问题了(就算不让你编写完整代码,也会让你描述大致的思路)。这个问题也难也难,说易也易,下面我就来对这个问题进行一个比较全面的解析吧。如有遗漏,还望指正。


版权说明

著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。

本文作者:Coding-Naga

发表日期: 2016年3月27日

本文链接:http://blog.csdn.net/lemon_tree12138/article/details/50986990

来源:CSDN

更多内容:分类 >> 算法与数学


描述

对于一个给定的序列 a = [a1, a2, a3, … , an],请设计一个算法,用于输出这个序列的全部排列方式。

例如:a = [1, 2, 3]

输出

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

如果要按从小到大输出呢?算法又要怎么写?


基于递归的实现

思路分析

我们知道全排列的含义就是一个序列所有的排序可能性,那么我们现在做这样的一个假设,假设给定的一些序列中第一位都不相同,那么就可以认定说这些序列一定不是同一个序列,这是一个很显然的问题。有了上面的这一条结论,我们就可以同理得到如果在第一位相同,可是第二位不同,那么在这些序列中也一定都不是同一个序列,这是由上一条结论可以获得的。

那么,这个问题可以这样来看。我们获得了在第一个位置上的所有情况之后,抽去序列T中的第一个位置,那么对于剩下的序列可以看成是一个全新的序列T1,序列T1可以认为是与之前的序列毫无关联了。同样的,我们可以对这个T1进行与T相同的操作,直到T中只一个元素为止。这样我们就获得了所有的可能性。所以很显然,这是一个递归算法。

例如下面这幅图,就是第1个元素与其后面的所有其他元素进行交换的示意图。

如果我们从中抽出第i个元素,将剩下的其余元素进行上图交换操作,将是如下示意图。

所有元素均无相同的情况

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

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); // 用于对之前交换过的数据进行还原
            }
        }
    }

基于非递归的实现

思路分析

由于非递归的方法是基于对元素大小关系进行比较而实现的,所以这里暂时不考虑存在相同数据的情况。

在没有相同元素的情况下,任何不同顺序的序列都不可能相同。不同的序列就一定会有大有小。也就是说,我们只要对序列按照一定的大小关系,找到某一个序列的下一个序列。那从最小的一个序列找起,直到找到最大的序列为止,那么就算找到了所有的元素了。

好了,现在整体思路是清晰了。可是,要怎么找到这里说的下一个序列呢?这个下一个序列有什么性质呢?

T[i]下一个序列T[i+1]是在所有序列中比T[i]大,且相邻的序列。关于怎么找到这个元素,我们还是从一个例子来入手吧。

现在假设序列T[i] = {6, 4, 2, 8, 3, 1},那么我们可以通过如下两步找到它的下一个序列。

看完上面的两个步骤,不知道大家有没有理解。如果不理解,那么不理解的点可能就在于替换点和被替点的寻找,以及之后为什么又要进行反转上。我们一个一个地解决问题吧。

- 替换点和被替换点的寻找。替换点是从整个序列最后一个位置开始,找到一个连续的上升的两个元素。前一个元素的index就是替换点。再从替换点开始,向后搜寻找到一个只比替换点元素大的被替换点。(如果这里你不是很理解,可以结合图形多思考思考。)

- 替换点后面子序列的反转。在上一步中,可以看到替换之后的子序列({8, 2, 1})是一个递减的序列,而替换点又从小元素换成 了大元素,那么与之前序列紧相邻的序列必定是{8, 2, 1}的反序列,即{1, 2, 8}。

这样,思路已经完全梳理完了,现在就是对其的实现了。只是为了防止给定的序列不是最小的,那就需要对其进行按从小到大进行排序。

逻辑实现

public class DemoFullArray2 {
    public static void main(String[] args) {
        int[] array = {2, 3, 1, 4};
        core(array);
    }

    private static void core(int[] array) {
        // 先排序
        SortUtils sortUtils = new SortUtils(new QKSort());
        sortUtils.sort(array);
        System.out.println(Arrays.toString(array)); // 最初始的序列
        do {
            nextArray(array);
            System.out.println(Arrays.toString(array));
        } while (!isLast(array));
    }

    private static int[] nextArray(int[] array) {
        int length = array.length;
        // 寻找替换点
        int cursor = 0;
        for (int i = length - 1; i >= 1; i--) {
            // 找到第一个递增的元素对
            if (array[i - 1] < array[i]) {
                cursor = i - 1; // 找到替换点
                break;
            }
        }

        // 寻找在替换点后面的次小元素
        int biggerCursor = cursor + 1;
        for (int i = cursor + 1; i < length; i++) {
            if (array[cursor] < array[i] && array[i] < array[biggerCursor]) {
                biggerCursor = i;
            }
        }

        // 交换
        ArrayUtils.swap(array, cursor, biggerCursor);

        // 对替换点之后的序列进行反转
        reverse(array, cursor);

        return array;
    }

    private static void reverse(int[] array, int cursor) {
        int end = array.length - 1;
        for (int i = cursor + 1; i <= end; i++, end--) {
            ArrayUtils.swap(array, i, end);
        }
    }

    private static boolean isLast(int[] array) {
        int length = array.length;
        for (int i = 1; i < length; i++) {
            if (array[i - 1] < array[i]) {
                return false;
            }
        }
        return true;
    }
}

Ref

时间: 2024-10-29 19:11:51

全排列算法的全面解析的相关文章

不会全排列算法(Javascript实现),我教你呀!

今天我很郁闷,在实验室凑合睡了一晚,准备白天大干一场,结果一整天就只做出了一道算法题.看来还是经验不足呀,同志仍需努力呀. 算法题目要求是这样的: Return the number of total permutations of the provided string that don't have repeated consecutive letters. Assume that all characters in the provided string are each unique.F

全排列算法 --javascript 实现

(function(){ var ret = new Array(); var A = function a(str){ if(str == undefined || str == null){return new Array();} if(str.length < 2) {return new Array(str);} if(str.length == 2) {return new Array(str[0]+str[1],str[1]+str[0]);} for(var k = 0;k <

使用哈希算法将数字解析为函数指针-一种架构方法

使用哈希算法将数字解析为函数指针: 这也算是最简单的,不会带有地址冲突的哈希了,哈希函数可以描述为: func = arr[index].func index为输入,根据输入的index,找到其对应的函数指针返回 这种架构虽然简单,但是在做测试时还是非常有用的 比如一种测试有几十项,我可以使用这种架构来实现自动轮巡测试,或者手动交互时输入一个Index,即可以去调用对应的测试函数 另外根据这个代码,还可以学习到函数指针的定义和使用: 定义:typedef int (*FuncPtr)(char

全排列算法(转)

列出全排列的初始思想: 解决一个算法问题,我比较习惯于从基本的想法做起,我们先回顾一下我们自己是如何写一组数的全排列的: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

两种常用的全排列算法(java)

问题:给出一个字符串,输出所有可能的排列. 全排列有多种算法,此处仅介绍常用的两种:字典序法和递归法. 1.字典序法: 如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"."52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0.2都不行,5可以,将5和2交换得到"956

字符串非重复全排列算法

[题目描述] 输入一个字符串,打印出该字符串中字符的所有排列. 例如输入字符串abc,则输出由字符a.b.c 所能排列出来的所有字符串 abc.acb.bac.bca.cab 和 cba. [分析] 从集合中依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,如此递归处理,从而得到所有元素的全排列.以对字符串abc进行全排列为例,我们可以这么做:以abc为例 固定a,求后面bc的排列:abc,acb,求好后,a和b交换,得到bac 固定b,求后面ac的排列:bac,bca,求好

全排列算法的递归与非递归实现

全排列算法的递归与非递归实现 全排列算法是常见的算法,用于求一个序列的全排列,本文使用C语言分别用递归与非递归两种方法实现,可以接受元素各不相同的输入序列. 题目来自leetcode: Given a collection of numbers, return all possible permutations. For example, [1,2,3] have the following permutations: [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3

java全排列算法实现 --- 小例子实现

Question:猜算式 看下面的算式: □□ x □□ = □□ x □□□ 它表示:两个两位数相乘等于一个两位数乘以一个三位数. 如果没有限定条件,这样的例子很多. 但目前的限定是:这9个方块,表示1~9的9个数字,不包含0. 该算式中1至9的每个数字出现且只出现一次! 比如: 46 x 79 = 23 x 158 54 x 69 = 27 x 138 54 x 93 = 27 x 186 ..... 请编程,输出所有可能的情况! 注意: 左边的两个乘数交换算同一方案,不要重复输出! 不同方

算法(全排列算法封装)

本算法是教材中的全排列方法之一,本人仅做封装,在此感谢发现算法和传播算法的大牛们. /// <summary> /// 全排列算法,算法原理:Perm(n)=[n]*Pern(n-1).N的全排列等于将N个数取一个放在第N个位置后,剩下的N-1个数做全排列. /// 这个算法的一个用途是进行行列式的展开和计算,这也是这次封装这个算法的目的. /// </summary> public class Permulation { /// <summary> /// 排列结果