zoukankan      html  css  js  c++  java
  • 打印全排列思路

    长篇慎入,点击直接跳看最后一段代码

    从n个不同的元素中取m个元素(m<=n),按照一定的顺序排列起来,

    叫做从n个不同元素取出m个元素的一个排列。

    当m=n时,所有的排列情况叫做全排列,比如3的全排列为:

    1 2 3
    1 3 2
    
    2 1 3
    2 3 1
    
    3 1 2
    3 2 1


    我们先从简单的开始,要求写出代码打印上面的排列情况即可,顺序可以不一致。

    分析过程:

    首先,我们如何把三位的数字打印出来呢,有两种方式:

    printf("%d
    " ,num);  //num=123

    第二种:

    printf("%d%d%d
    ",a,b,c); //a=1 b=2 c=3

    我认为采用第二种比较好,原因在于第一种需要对位数的考虑,

    而我们问题需要对数字位置不断的进行交换,因此第二种也方便于交换。

    现在知道了如何打印,那么如何交换数字的位置呢?很简单:

    void swap(int *a,int *b){
      int tmp = *a;
      *a = *b;
      *b = tmp;
    }

    于是将上面的交换打印结合起来就可以,大概写个模型了

    void output(int a,int b,int c){
       printf("%d%d%d
    ",a,b,c);
       swap(&a,&b);
    output(a,b,c); //递归 }

    如果此时执行 output(1,2,3),那么程序将会死循环打印出下面的片段

    123
    213
    ..

    因此我们有两个问题要解决:

    1.怎样交换使得所有全排列都能被遍历到?

    2.怎样使得递归能够终止?

    从上面的死循环输出可以观察到,

    交换仅仅发生在 第一个数字第二个数字 之间。

    如果我们尝试继续对 第二个数字 第三个数字 进行交换。

    然后递归重复上面这个过程,

    1 2 3   //初始值,第一个周期开始
    
    2 1 3   //前两个数字做交换
    2 3 1   //后两个数字做交换
    
    ...  递归  ...
    
    3 2 1   //前两个数字做交换
    3 1 2   //后两个数字做交换
    
    ...  递归  ...
    
    1 3 2   //前两个数字做交换
    1 2 3   //后两个数字做交换,进入下一个周期
    ...  递归  ...

    ... 无限循环 ...

    你会发现所有的排列就可以被打印出来了,但是个死循环的打印。

    如何终止?

    只需要找到一个周期的开始特征即可,比如在上面的打印中

    初始值是 1 2 3

    而所有全排列情况打印完后,下个周期的开始也是 1 2 3

    那么可以来个判断a b c 变量是否同时和一开始一样,如果是就退出函数返回。

    解决两个问题后,修改一下output函数:

    void output(int a,int b,int c){
       
       printf("%d%d%d
    ",a,b,c); //打印初始值
    
       swap(&a,&b);    //交换前两个数字
       printf("%d%d%d
    ",a,b,c);
    
       swap(&b,&c);    //交换后两个数字
    
       //一个周期结束的情况发生在这个位置
       if(a==1 && b==2)return;
    
       output(a,b,c);   //递归
    
    }

    然后在main函数里面 执行 output(1,2,3) 输出结果如下:

    当然为了好看,你可以把交换的顺序改下,先交换后面两个数字,再交换首尾两个数字

    现在,3的全排列打印出来了,现在我们尝试写个代码可以打印任意一个数的全排列

    首先,假如打印4的全排列,如果继续使用上面的代码思路,那么将需要修改:

    output(1,2,3)   =>  output(1,2,3,4);
    
    printf("%d%d%d
    ")  =>  printf("%d%d%d%d
    ");  

    很多地方需要像上面一样添加一个变量,这样的话打印100的全排列,岂不是要写100个变量 或者 参数??

    这样很显然不科学,因此我们换一个问题:输入一个数字,然后打印这个数字的全排列。也就是:

    上面原版是:输入三个数字,打印这三个数字全排列,output(1,2,3);

    而现在改成:输入一个数字,打印这个数字的全排列,output(3),需要输出结果与上面相同。

    现在参数变成了一个,要通过这个参数能将1 2 3 成员表示出来

    要能交换这三个数字的其中两个,且不能使用变量多个变量,如何实现呢?

    首先我们需要把 1 2 3 这三个数字通过 3 这个数字得到,并将其保存到数组里,很简单:

    int i,arr[1024];
    for(i=1;i<=3;i++)
       arr[i] = i;   //为了方便操作我们从1开始存储

    然后遍历数组,对这个数组遍历输出一次,然后交换一次

    int i,j;
    for(i=1;i<3;i++){
     
       //输出当前的数字组合
       for(j=1;j<=3;j++)
          printf("%d",arr[j]);
       printf("
    ");
    
       //交换数组两个数字
       swap(&arr[i],&arr[i+1]);
    
    }

    我们尝试执行 output(3)  ,结果输出如下

    我们稍微做下修改,

    1、当 i 遍历到数组的最后一个元素时,与第一个元素交换

    2、将 3 统一用参数n来表示

    于是output函数就可以写成

    void output(int n){
      int i,j,arr[1024];
      for(i=1;i<=n;i++)    //保存n的全排列成员
         arr[i] = i;
    
      for(i=1;i<=n;i++){   //遍历数组

    //输出当前数组组合
    for(j=1;j<=n;j++) printf("%d",arr[j]); printf(" ");

    //交换数组的成员位置 if(i==n) swap(&arr[i],&ar
    r[1]); //当遍历到最后一个时,与第一个成员做交换
    else swap(
    &arr[i],&arr[i+1]); //否则与下一个元素做交换 } }

    那么假如n=3,将数组遍历一次的过程如下:

    1 2 3  //初始状态 , i=1
    2 1 3  //交换,i=2
    2 3 1  //交换,i=3
    1 3 2  //交换,i=4,退出循环

    很显然遍历一次是不足以将所有的排列情况输出出来。

    为了使得数组能够继续从下标1为开始遍历,思路就是使用递归

    也就是第一次遍历完后,将遍历完后的数组 递归传给函数本身继续遍历

    而要保持数组的状态,意味着数组要以参数的形式传递,其次数组的初始化不能再递归里初始。

    所以初始化可以放在main函数里,当然既然用到了递归,就需要防止无休止的递归,也就是要找到退出状态

    不妨我们再写代码之前分析一下:

    传入数组
    arr = [0, 1, 2, 3]  //0下标不使用,下面忽略
    
    //开始遍历
    [ 1, 2, 3]    // i=1,输出然后交换
    [ 2, 1, 3]    // i=2,输出然后交换
    [ 2, 3, 1]    // i=3,输出然后与第一个元素交换
    [ 1, 3, 2]    // i=4,  退出循环
    
    -----递归-----
    
    传入数组
    arr = [0, 1, 3, 2]  //保存上次的状态继续任务,0继续忽略
    
    //开始遍历
    [ 1, 3, 2]    // i=1 输出然后交换
    [ 3, 1, 2]    // i=2 输出然后交换
    [ 3, 2, 1]    // i=3 输出然后与第一个元素交换
    [ 1, 2, 3]    // i=4 退出循环,至此我们不需要继续执行了
    
    ---因此需要在此写个递归结束判断条件----

    递归的结束条件与上面思路一样,如果数组与最最开始的状态是一样的

    那么表示一个周期已经完成:

    int tag = 1;          //标志位
    for(i=1;i<=n;i++){
       if(arr[i]!=i){     //如果存在不同继续递归
         tag = 0;
         break;
      }
    }
    if(tag)return //如果全相同退出递归

    那么总的代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    #define MAX 1024
    
    void swap(int *a,int *b){
      int tmp = *a;
      *a = *b;
      *b = tmp;
      return;
    }
    void output(int n,int arr[]){
      int i,j;
      for(i=1;i<=n;i++){
    
        //print
        for(j=1;j<=n;j++){
          printf("%d ",arr[j]);
        }
        printf("
    ");
    
        //swap
        if(i==n)swap(&arr[i],&arr[1]);
        else swap(&arr[i],&arr[i+1]);
    
      }
    
      int tag = 1;
      for(i=1;i<=n;i++)
        if(arr[i]!=i){tag=0;break;}
    
      if(tag)return;
    
      output(n,arr);
    }
    
    int main(){
      int i;
      int n,arr[MAX];
      printf("enter a num:");
      scanf("%d",&n);
      for(i=1;i<=n;i++)
        arr[i]=i;
    
      output(n,arr);
      return 0;
    }

    执行结果

    对于高位数的全排列,上面的思路不适合,其实打印全排列主要需要考虑的是如何安排数字位置之间

    的交换,使得每种情况都可以遍历到,想要避免非重复遍历是不可能的,除非有一个很复杂的函数计算

    公式,否则单纯的位置交换一定会有不断的重复排列某一种状态,但只要保证打印出来的时候,每个打印

    结果不想同即可,如下一段代码,打印7的全排列, 7*6*5*4*3*2*1 = 5040种情况,由于本人能力有限

    设计不出来一个可以打印全排列的完美情况,所以下面参考网上文章的代码:

    #include <stdio.h>
    #include <stdlib.h>
    int n=0;
    
    void swap(int *a, int *b){
         int m;
         m=*a;
         *a=*b;
         *b=m;
    }
    void perm(int list[], int k, int m){
         int i;
         if(k==m){
           for(i=0;i<=m;i++)
             printf("%d ",list[i]);
             printf("
    ");
             n++;
         } else {
           for(i=k;i<=m;i++){
             swap(&list[k],&list[i]);
             perm(list, k+1, m);
             swap(&list[k], &list[i]);
           }
         }
    }       
    int main(void){
        int list[]={1,2,3,4,5,6,7};
        perm(list,0,6);
        printf("total:%d
    ",n);return 0;
    }

    代码运行时流程如下:

    1.只有k=6时才能够打印
    2.递归到最深处时,k=6
    3.利用循环使得k及其身后的数字两两相邻交换
    4.弹出条件是k后面的数字都完成了相邻交换,也恢复了相对初状态
    5.每次弹出后马上要做的就是恢复状态
    6.不断回溯k重复上面的步骤


    一开始 递归k直至进入6  打印初状态 1,2,3,4,5,6,7

    回溯 k 到5 ,利用循环让 i=6 交换5,6 递归到底打印出来,弹出再把5,6换回来 回到初状态    打印:1,2,3,4,5,7,6    换回:1,2,3,4,5,6,7

    回溯 k 到4 ,利用循环让 i=5 交换4,5 递归到底打印出来                              打印:1,2,3,4,6,5,7

    弹出 k 到5 ,利用循环让 i=6 交换5,6 递归到底打印出来,弹出再把 5,6 换回来             打印:1,2,3,4,6,7,5    换回:1,2,3,4,6,5,7

    弹出 k 为4 ,换回4和5,继续循环                                                 换回:1,2,3,4,5,6,7

    k=4 循环使得 i=6 交换4,6值,递归到底打印出来,                                     打印:1,2,3,4,7,6,5    

    弹出 k 到5 ,利用循环让 i=6 交换5,6 递归到底打印出来,弹出再把5,6换回来                打印:1,2,3,4,7,5,6    换回:1,2,3,4,7,6,5

    弹出 k 到4 ,换回4,6,循环结束,完成任务继续回溯                                 换回:1,2,3,4,5,6,7

    以此类推。。。

    下面是部分全排列截图

  • 相关阅读:
    LeetCode 23. Merge k Sorted Lists
    第四章 基本TCP套接字编程 第五章 TCP客户/服务器程序实例
    LeetCode 18. 4Sum
    LeetCode 16. 3Sum Closest
    Leetcode题 257. Binary Tree Paths
    Django---Form表单
    Python---面向对象(二)
    Python---面向对象(一)
    Django---Cookie && Session && 分页
    [ Day51 ]Python之路----JavaScript --DOM操作
  • 原文地址:https://www.cnblogs.com/demonxian3/p/7496813.html
Copyright © 2011-2022 走看看