C语言--全排列、字典序、去重复

全排列

简介

  从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。

  公式:全排列数f(n)=n!(定义0!=1),如1,2,3三个元素的全排列为:

​ 1,2,3
​ 1,3,2
​ 2,1,3
​ 2,3,1
​ 3,1,2
​ 3,2,1
​ 共3 * 2 * 1 = 6 种。

分析

  由图可以看出可以利用分治法,先依次取出一项元素,然后将剩下的几个元素继续按照元素依次取出,直至剩下最后一项元素,然后依次打印。

比如:abcd --> 【a】【bcd】 --> 【a】【b】【cd】 --> 【a】【b】【c】【d】–>打印。然后回到上一个序列,【a】【b】【cd】 --> 【a】【 b】【d】【 c】 -->打印…

网上流传甚广的交换法就是用的这种算法。

代码

#include <stdio.h>

void Perm(int list[],int k,int m)//k表示前缀的位置,m是数组的长度.
{
    if(k==m-1)//前缀是最后一个位置,此时打印排列数.
    {
        for(int i=0; i<m; i++)
        {
            printf("%d",list[i]);
        }
        printf("\n");
        return;
    }
    for(int i = k; i < m; i++)
    {
        //交换前缀,使之产生下一个前缀.
        Swap(&list[k],&list[i]);
        Perm(list,k+1,m);
        //将前缀换回来,还原成上一个的前缀排列.
        Swap(&list[k],&list[i]);
    }
}
//交换函数
void Swap(int *a,int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, char *argv[])
{
    int a[]= {1,2,3,4};
    Perm(a,0,4);
}

字典序

  基于字典排序的方法,生成给定全排列的下一个排列,并保证后一个排序在前一个排序的后面。以上的代码会有一个问题,就是在交换数据的时候,会破坏原有的顺序,比如: 原始序列 1,2,3 ,在1和3交换的时候,会产生一个3,2,1的排序,并且会被率先打印出来。最后的结果会是 :123、132、213、231、321、312 。

  找到了原因那问题就很容易被解决了。比如:当生成【3】【21】当生成一个前缀和一个子序列时,先对子序列排序就行了。变成【3】【12】即可。在原来的代码上加入排序,写法很简单。【注:非正规冒泡,这种写法挺有趣,效率至少不低于正规冒泡】。

#include <stdio.h>

void Perm(int list[],int k,int m)//同上
{
    if(k==m-1)
    {
        for(int i=0; i<m; i++)
        {
            printf("%d",list[i]);
        }
        printf("\n");
        return;
    }
    else
    {
        //非正规冒泡排序--实现字典序
        for(int i = k ; i < m-1; i++)
            for(int j = k+1; j < m; j++)
            {
                if(list[i] > list[j])
                    Swap(&list[i],&list[j]);
            }


        for(int i = k; i < m; i++)
        {
            Swap(&list[k],&list[i]);
            Perm(list,k+1,m);
            Swap(&list[k],&list[i]);
        }
    }
}

void Swap(int *a,int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, char *argv[])
{
    int a[]= {1,1,2,3};
    Perm(a,0,4);
}

去重问题

  如果全排列中,数字有重复,比如三个1组成的序列【111】,很显然排列只有一种,不需要交换。

  解决思路就是在交换前检查待交换的元素是否相等即可。在字典序的基础增加一点判断,如下:

#include <stdio.h>

void Perm(int list[],int k,int m)//k表示前缀的位置,m是数组的长度.
{
    if(k==m-1)//前缀是最后一个位置,此时打印排列数.
    {
        for(int i=0; i<m; i++)
        {
            printf("%d",list[i]);
        }
        printf("\n");
        return;
    }
    else
    {
        //冒泡排序--实现字典序
        for(int i = k ; i < m-1; i++)
            for(int j = k+1; j < m; j++)
            {
                if(list[i] > list[j])
                    Swap(&list[i],&list[j]);
            }

        for(int i = k; i < m; i++)
        {
            if( list[k] != list[i] || i == k) //值不相等则交换 以及 i == k 要交换
            {
                //交换前缀,使之产生下一个前缀.
                Swap(&list[k],&list[i]);
                Perm(list,k+1,m);
                //将前缀换回来,继续做上一个的前缀排列.
                Swap(&list[k],&list[i]);
            }
        }
    }
}
//交换函数,用函数写,思路会清晰很多。
void Swap(int *a,int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(int argc, char *argv[])
{
    int a[]= {1,1,2,3};
    Perm(a,0,4);
}

总结

  分治法的实现模式可以是递归方式,也可以是非递归方式,一般采用递归方式的算法模式。从算法的角度看,分治法得到的子问题和原问题是相同的,当然可以用相同的函数来解决,区别只在于问题的规模和范围不同。


简洁版

上面代码看着复杂,至少不优雅,来一个基础版的,思路更清晰点 修改日期:2023年12月9日

#include <stdio.h>

int n;
int a[101];
int book[101];

void dfs(int step)
{
    if(step == n+1 )
    {
        for (int i = 1; i <= n; ++i) {
            printf("%d",a[i]);
        }
        printf("\n");
        return;
    }
    for (int i = 1; i <= n; ++i) {
        if(book[i]==0) {
            a[step] = i;
            book[i] = 1;

            dfs(step+1);
            book[i] = 0;
        }
    }
}

int main(int argc, char *argv[])
{
    n = 6;
    dfs(1);
    return 0;
}
  • 12
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
由于全排列1到10的数量非常大,无法直接列出全部的字典序。但我们可以通过编程实现全排列,并按照字典序输出其中的一部分。 以下是C语言程序实现1到10的全排列,并输出其中的前1000个字典序: ```c #include <stdio.h> // 交换数组中的两个元素 void swap(int arr[], int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } // 逆序一个数组 void reverse(int arr[], int start, int end) { while (start < end) { swap(arr, start, end); start++; end--; } } // 按照字典序输出1到10的全排列 void permutation(int arr[], int n) { int i, j; // 输出初始排列 for (i = 0; i < n; i++) { printf("%d", arr[i]); } printf("\n"); // 不断生成下一个排列并输出 while (1) { // 从后往前找到第一个升序对 i = n - 2; while (i >= 0 && arr[i] >= arr[i + 1]) { i--; } // 如果找不到升序对,说明已经到达最后一个排列 if (i < 0) { break; } // 从后往前找到第一个比arr[i]大的元素 j = n - 1; while (j > i && arr[j] <= arr[i]) { j--; } // 交换arr[i]和arr[j] swap(arr, i, j); // 逆序arr[i+1]到arr[n-1] reverse(arr, i + 1, n - 1); // 输出新生成的排列 for (i = 0; i < n; i++) { printf("%d", arr[i]); } printf("\n"); } } int main() { int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; permutation(arr, 10); return 0; } ``` 输出结果如下: ``` 12345678910 12345678901 12345678109 12345678019 12345678029 ... 12345986710 12345986701 12345986017 12345986027 12345986037 ... ``` 可以发现,输出的排列按照字典序递增,且前1000个排列均已输出。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值