数组的全排列算法

目录

 

全排列算法 

代码如下

最详细的理解

确定递推公式

 第一位的排列情况


 

全排列算法 

      全排列算法是一种经典的递归算法。例如集合{a,b,c}的全排列为{(a,b,c)、(a,c,b)、(b,a,c)、(b,c,a)、(c,b,a)、(c,a,b)}共3!种。

  递归法求解的思路是先固定第一个元素,求剩下的全排列,求剩下的全拍列时,固定剩余元素中的第一个元素,再求剩下元素的全排列,直到就剩一个元素停止。

  例如求集合{a,b,c,d}的全排列。

  1、固定元素a求{b,c,d}元素的全排列

    (1)、固定元素b求{c,d}的全排列

      1)、固定元素c ,得到一个排列方式(a,b,c,d)

      2)、固定元素d,得到一种排列方式(a,b,d,c)

    (2)、固定元素c求{b,d}的全排列

      1)、固定元素b,得到一个排列方式(a,c,b,d)

      2)、固定元素d,得到一种排列方式(a,c,d,b)

    (3)、固定元素d求{b,c}的全排列

      1)、固定元素b,得到一个排列方式(a,d,b,c)

      2)、固定元素c,得到一种排列方式(a,d,c,b)

  经过上述步骤即可得到以a为第一个元素的全排列,再分别将b,c,d固定为第一元素重复上面过程即可得到{a,b,c,d}的全排列

 

代码如下

#include <iostream>

using namespace std;

int count = 0;   //计数全排列的个数
void perm(char A[], int start, int end)//A是要排列的数组,start、end表示对A[start]与A[end]之间的元素进行全排列
{
    if (start == end)
    {
        for (int i = 0;i <= end; i++)
            cout << A[i] << "  ";
        cout << endl;
        count++;
    }
    else
    {
        for (int i = start; i <= end; i++)
        {
            swap(A[i],A[start]);
            perm(A, start + 1, end);
            swap(A[i], A[start]);
        }
    }
}
int main()
{
    char A[10]={"abcdefg"};
    perm(A,0,2);  //start = 0,end = 2表示对A[0]与A[2]之间的元素进行全排列,即对{a,b,c}进行全排列
    cout<< count <<endl;
    return 0;
}

 

Python示例:

def quan(a,s):
    if s==len(a):
        print(a)
        return

    for i in range(s,len(a)):
        a[s],a[i] = a[i],a[s]
        quan(a,s+1)
        a[s],a[i] = a[i],a[s]
    return

a = [1,2,3]
quan(a,0)

算法思路:

假设我们要对1,2,3,4四个数进行全排列,过程如下:
  (a)首先保持1不变,对2,3,4全排列;
  (b)保持2不变,对3,4全排列;
  (c)保持3不变,对4全排列,4的排列只有一种。得到1,2,3,4
  (d)然后3不能不变了,继续保持2不变,3,4互换得到1,2,4,3
  (e)以1,2打头的排列完成,接下来把3换到2的位置,继续(c)、(d)的操作
  ……
 得到1,3,2,4
   1,3,4,2
   1,4,3,2
   1,4,2,3
  因此得到以1打头的全部排序,以此类推,得到以2,3,4打头的排序,得到全排序。

将以上过程总结成一个递归算法:

  任取一个数打头,对后面n-1个数进行全排序,要求n-1个数的全排序,则要求n-2个数的全排序……直到要求的全排序只有一个数,找到出口。

伪代码:

 

m到n的全排序
Permutation(m,n){
if:全排列只有一个数,输出排列
  else: 
    for{i=m;i<n;i++}{//i遍历第m~n个数,每次以a[i]所存的数值为打头的数
        swap(a[m],a[i]);//把要打头的数放到最开头的位置(即m所在的位置)
        Permutation(m+1,n);//递归
        swap(a[m],a[i]);//为避免重复排序,每个数打头结束后都恢复初始排序,防止重复的方法很多,不止这一种
     }
}

从第m个元素到第n个元素的全排列代码:

 

void Permutation(int a[],int m,int n){
    if(m==n){
        cout<<a[0];
        for(int i=1;i<n;i++){
            cout<<" "<<a[i];
        }
        cout<<endl;
    }
    else {
        for(int i=m;i<n;i++){
            int temp=a[m];
            a[m]=a[i];
            a[i]=temp;
            Permutation(a,m+1,n);
            temp=a[m];
            a[m]=a[i];
            a[i]=temp;
        }
    }
}

最详细的理解:

笔试面试算法经典–全排列算法-递归&字典序实现(Java)
全排列算法的全面解析

看了上面两篇对全排列算法应该就有些感觉了,对递归的解法我一开始看得还是挺蒙的,看了好一会才明白。

首先,递归问题一定先演算出其递推公式,找到终止条件。

我一开始在学习递归的时候,在纸上画出递归的过程,在脑子中像计算机一样回放一个个递归步骤,我发现这样反而进入了思维误区,给自己制造了理解障碍。递归就是将大问题分解为子问题,将最终的子问题的解作为终止条件,而这些子问题和大问题的解法都是一样的,只需要思考大问题和子问题之间的关系就行,不需要一层层往下去思考子问题与子子问题之间的关系。这样大脑一下就轻松了,原来这才是本质。

摘自极客时间王争老师的《数据结构与算法之美》专栏:

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。

编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

下面来一步步解析全排列算法的递归解法。

确定递推公式

假设有 {1, 2, 3, ... n} 这样一个序列,现在要找出这个序列的全排列,第一位有 n 种可能性,确定了第一位后就是求解剩下 n - 1 个数据的排列问题,这样就可以往下一直分解问题,直到序列结尾处,也就是终止条件。这样递推公式就可以表示成:

f(1, 2, ... n) = {第一位是 1, f(n-1)} + {第一位是 2, f(n-1)} +...+{第一位是 n, f(n-1)}

 第一位的排列情况

数组 {1, 2, 3, 4},第一位有 4 种可能性:

1, 2, 3, 4
2, 1, 3, 4
3, 2, 1, 4
4, 2, 3, 1

 就是将第一位和后面的数依次交换,交换之前的序列是 {1, 2, 3, 4}:

public class PermutationAdvanced {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4};
        allPermutation(a, 0,a.length - 1);

    }

    private static void allPermutation(int[] a, int cursor, int k) {
        for (int i = cursor; i <= k; i++) {
            swap(a, cursor, i);
            System.out.println(Arrays.toString(a));
            // 保证交换之前的序列还是 {1, 2, 3, 4}
            swap(a, cursor, i);
        }
    }

    private static void swap(int[] a, int cursor, int i) {
        int temp = a[cursor];
        a[cursor] = a[i];
        a[i] = temp;
    }
}

 输出:

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

 

重复元素的序列

序列 {3, 3, 2, 3, 4} 在上面的代码中输出是:

[3, 3, 2, 3, 4]
[3, 3, 2, 3, 4]
[2, 3, 3, 3, 4]
[3, 3, 2, 3, 4]
[4, 3, 2, 3, 3]

 这个序列第一位只有 3 种情况:3,2,4。所以还得针对存在重复元素的序列改进代码:

public class PermutationAdvanced {
    public static void main(String[] args) {
//        int[] a = {1, 2, 3, 4};
        int[] a = {3, 3, 2, 3, 4};
        allPermutation(a, 0, a.length - 1);

    }

    private static void allPermutation(int[] a, int cursor, int k) {
        for (int i = cursor; i <= k; i++) {
            if (!judgeSwap(a, cursor, i)) {
                continue;
            }
            swap(a, cursor, i);
            System.out.println(Arrays.toString(a));
            // 保证交换之前的序列还是 {1, 2, 3, 4}
            swap(a, cursor, i);
        }
    }

    private static void swap(int[] a, int cursor, int i) {
        int temp = a[cursor];
        a[cursor] = a[i];
        a[i] = temp;
    }

    /**
     * 判断是否需要进行交换
     *
     * @param a
     * @param cursor
     * @param i
     * @return
     */
    private static boolean judgeSwap(int[] a, int cursor, int i) {
        for (int j = cursor; j < i; j++) {
            /**
             * a[i] 是等待被交换的元素
             * 如果 cursor == i 需要进行交换
             * 如果 在 [cursor, i) 范围里存在和 a[i] 相同的元素则不进行交换,说明这种情况已经存在了
             */
            if (a[j] == a[i]) {
                return false;
            }
        }
        return true;
    }
}

 输出:

[3, 3, 2, 3, 4]
[2, 3, 3, 3, 4]
[4, 3, 2, 3, 3]

 加入递归

通过代码将序列第一位的所有情况的罗列出来了,序列剩下的 n - 1 或 n - 2 或 n - 3… 排列情况和第一位是一样的:

 

public class PermutationAdvanced {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
//        int[] a = {3, 3, 2, 3, 4};
        allPermutation(a, 0, a.length - 1);

    }

    private static void allPermutation(int[] a, int cursor, int k) {
        // 递归终止条件
        // 已经到序列结尾了 
        if (cursor == k) {
            System.out.println(Arrays.toString(a));
        }
        for (int i = cursor; i <= k; i++) {
            if (!judgeSwap(a, cursor, i)) {
                continue;
            }
            swap(a, cursor, i);
            allPermutation(a, cursor + 1, k);
            // 保证交换之前的序列还是 {1, 2, 3, 4}
            swap(a, cursor, i);
        }
    }

    private static void swap(int[] a, int cursor, int i) {
        int temp = a[cursor];
        a[cursor] = a[i];
        a[i] = temp;
    }

    /**
     * 判断是否需要进行交换
     *
     * @param a
     * @param cursor
     * @param i
     * @return
     */
    private static boolean judgeSwap(int[] a, int cursor, int i) {
        for (int j = cursor; j < i; j++) {
            /**
             * a[i] 是等待被交换的元素
             * 如果 cursor == i 需要进行交换
             * 如果 在 [cursor, i) 范围里存在和 a[i] 相同的元素则不进行交换,说明这种情况已经存在了
             */
            if (a[j] == a[i]) {
                return false;
            }
        }
        return true;
    }
}

时间复杂度

第一层分解有 n 次交换操作;
第二层有 n 个节点,每个节点 n - 1 次交换,第二层交换次数为 n * (n - 1);
第三层的节点数就是上一层的交换次数 n * (n - 1),每个结点交换 n - 2 次,第三层的交换次数为 n * (n - 1) * (n - 2)

那第 k 层交换次数可以 表示成:n * (n - 1) * (n - 2) * ... * (n - k + 1)
那总的交换次数就是各层加起来:

n + n * (n - 1) + n * (n - 1) * (n - 2) + ... + n * (n - 1) * (n - 2) * ... * 2 * 1

 

最后一个数 n * (n - 1) * (n - 2) * ... * 2 * 1 等于 n!,前面的每个数肯定都是小于 n!,那这个公式的和小于 n * n!,可以看出全排列递归的时间复杂度是大于 O (n!),小于O(n * n!)的,时间复杂度还是挺高的哦。

总结

如果要通过人脑去过一遍递归的过程,那是很困难的,当然也没这个必要。求解的递归的关键点是:

  1. 一个问题是否可以分解为多个子问题,然后子问题又可以继续划分;
  2. 分解后的子问题除了数据规模不一样,但具体解法还是一样。在全排列的求解过程中,每个子问题的解法一样,那就先解出一个子问题(求第一位有多少种情况),然后再加入递归代码,大脑中不用去模拟递归的一个过程,你就想子问题的解法都是一样的,计算机只不过是在通过栈做重复的事情而已;
  3. 找到子问题终止条件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值