全排列和全组合实现

记得@老赵之前在微博上吐槽说,“有的人真是毫无长进,六年前某同事不会写程序输出全排列,昨天发邮件还是问我该怎么写,这时间浪费到我都看不下去了。” 那时候就很好奇全排列到底是什么东西,到底有多难?

今天复习的时候终于碰到这题了,结果果然自己太渣,看了好久都没明白,代码实现又是磕磕碰碰的。所以,就把它整理成笔记加深记忆,也希望能帮到和我一样的人。

全排列

所谓全排列,就是打印出字符串中所有字符的所有排列。例如输入字符串abc,则打印出 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba 。

一般最先想到的方法是暴力循环法,即对于每一位,遍历集合中可能的元素,如果在这一位之前出现过了该元素,跳过该元素。例如对于abc,第一位可以是 a 或 b 或 c 。当第一位为 a 时,第二位再遍历集合,发现 a 不行,因为前面已经出现 a 了,而 b 和 c 可以。当第二位为 b 时 , 再遍历集合,发现 a 和 b 都不行,c 可以。可以用递归或循环来实现,但是复杂度为 O(nn) 。有没有更优雅的解法呢。

用golang实现的暴力循环全排列求法:add by [email protected]

func FullPermutationCycle(in string) (ret []string) {
    num := len(in)
    orgIns := []byte(in)
    var reslut [][]byte
    for i := 0; i < num; i++ {
        reslut = append(reslut, []byte{orgIns[i]}) //插入的第一个元素,依次可以为字符串中的每个字符
    }
    fmt.Printf("%v\n", reslut)

    for i := 1; i < num; i++ { //依次遍历后续可插入的位置,并同时依次查询已经插入的字符中是否已经存在该字符,如果已经存在,就不插入了,否则可以插入字符
        //记录一个存储的中间过程
        var midRelsut [][]byte
        for _, existV := range reslut { //取已经插入的字符出来,进行判断
            for j := 0; j < num; j++ { //对输入的字符,进行遍历
                if !bytes.Contains(existV, []byte{orgIns[j]}) { //如果不包含这个字符,就插入
                    tmp := make([]byte, len(existV))
                    copy(tmp, existV)
                    tmp = append(tmp, orgIns[j])
                    midRelsut = append(midRelsut, tmp)
                }
            }
        }
        reslut = midRelsut
    }
    fmt.Printf("=====%v\n", reslut)
    //转换为字符串
    for _, cur := range reslut {
        strCur := string(cur)
        ret = append(ret, strCur)
    }

    return
}
func main() {
    per := FullPermutationCycle("abc")
    fmt.Printf("%v", per)
}

首先考虑bac和cba这二个字符串是如何得出的。显然这二个都是abc中的 a 与后面两字符交换得到的。然后可以将abc的第二个字符和第三个字符交换得到acb。同理可以根据bac和cba来得bca和cab。

因此可以知道 全排列就是从第一个数字起每个数分别与它后面的数字交换,也可以得出这种解法每次得到的结果都是正确结果,所以复杂度为 O(n!)。找到这个规律后,递归的代码就很容易写出来了:

#include<stdio.h>
#include<string>
//交换两个字符
void Swap(char *a ,char *b)
{
    char temp = *a;
    *a = *b;
    *b = temp;
}
//递归全排列,start 为全排列开始的下标, length 为str数组的长度
void AllRange(char* str,int start,int length)
{
    if(start == length-1)
    {
        printf("%s\n",str);
    }
    else
    {
        for(int i=start;i<=length-1;i++)
        {    //从下标为start的数开始,分别与它后面的数字交换
            Swap(&str[start],&str[i]);
            AllRange(str,start+1,length);
            Swap(&str[start],&str[i]);
        }
    }
}
void Permutation(char* str)
{
    if(str == NULL)
        return;
    AllRange(str,0,strlen(str));
}
void main()
{
    char str[] = "abc";
    Permutation(str);
}

去重的全排列

为了得到不一样的排列,可能我们最先想到的方法是当遇到和自己相同的就不交换了。如果我们输入的是abb,那么第一个字符与后面的交换后得到 bab、bba。然后abb中,第二个字符和第三个就不用交换了。但是对于bab,它的第二个字符和第三个是不同的,交换后得到bba,和之前的重复了。因此,这种方法不行。

因为abb能得到bab和bba,而bab又能得到bba,那我们能不能第一个bba不求呢? 我们有了这种思路,第一个字符a与第二个字符b交换得到bab,然后考虑第一个字符a与第三个字符b交换,此时由于第三个字符等于第二个字符,所以它们不再交换。再考虑bab,它的第二个与第三个字符交换可以得到bba。此时全排列生成完毕,即abb、bab、bba三个。

这样我们也得到了在全排列中去掉重复的规则:去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。用编程的话描述就是第i个数与第j个数交换时,要求[i,j)中没有与第j个数相等的数。下面给出完整代码:

#include<stdio.h>
#include<string>
//交换两个字符
void Swap(char *a ,char *b)
{
    char temp = *a;
    *a = *b;
    *b = temp;
}
//在 str 数组中,[start,end) 中是否有与 str[end] 元素相同的
bool IsSwap(char* str,int start,int end)
{
    for(;start<end;start++)
    {
        if(str[start] == str[end])
            return false;
    }
    return true;
}
//递归去重全排列,start 为全排列开始的下标, length 为str数组的长度
void AllRange2(char* str,int start,int length)
{
    if(start == length-1)
    {
        printf("%s\n",str);
    }
    else
    {
        for(int i=start;i<=length-1;i++)
        {
            if(IsSwap(str,start,i))
            {
                Swap(&str[start],&str[i]);
                AllRange2(str,start+1,length);
                Swap(&str[start],&str[i]);
            }
        }
    }
}
void Permutation(char* str)
{
    if(str == NULL)
        return;
    AllRange2(str,0,strlen(str));
}
void main()
{
    char str[] = "abb";
    Permutation(str);
}

全组合

如果不是求字符的所有排列,而是求字符的所有组合应该怎么办呢?还是输入三个字符 a、b、c,则它们的组合有a b c ab ac bc abc。当然我们还是可以借鉴全排列的思路,利用问题分解的思路,最终用递归解决。不过这里介绍一种比较巧妙的思路 —— 基于位图。

假设原有元素 n 个,则最终组合结果是 2n−1 个。我们可以用位操作方法:假设元素原本有:a,b,c 三个,则 1 表示取该元素,0 表示不取。故取a则是001,取ab则是011。所以一共三位,每个位上有两个选择 0 和 1。而000没有意义,所以是2n−1个结果。

这些结果的位图值都是 1,2…2^n-1。所以从值 1 到值 2n−1 依次输出结果:

001,010,011,100,101,110,111 。对应输出组合结果为:a,b,ab,c,ac,bc,abc。

因此可以循环 1~2^n-1,然后输出对应代表的组合即可。有代码如下:

#include<stdio.h>
#include<string.h>
void Combination(char *str)
{
    if(str == NULL)
        return ;
    int len = strlen(str);
    int n = 1<<len;
    for(int i=1;i<n;i++)    //从 1 循环到 2^len -1
    {
        for(int j=0;j<len;j++)
        {
            int temp = i;
            if(temp & (1<<j))   //对应位上为1,则输出对应的字符
            {
                printf("%c",*(str+j));
            }
        }
        printf("\n");
    }
}
void main()
{
    char str[] = "abc";
    Combination(str);
}

参考资料

本文大部分内容源自:http://wuchong.me/blog/2014/07/28/permutation-and-combination-realize/

原文地址:https://www.cnblogs.com/lihaiping/p/9525870.html

时间: 2024-10-07 04:50:57

全排列和全组合实现的相关文章

全排列,全组合(dfs)

全排列 #include <iostream> #include <cmath> #include <cstdio> #include <cstring> #include <string> #include <map> #include <iomanip> #include <algorithm> #include <queue> #include <stack> #include &

字符串的全排列和全组合

输入:abc 输出:bac,cba,acb,bca,cab,abc 全排列的问题: public ArrayList<String> Permutation(String str) { ArrayList<String> list = new ArrayList<String>(); if(str!=null && str.length()>0){ helper(str.toCharArray(),list,0); Collections.sort

常见算法之全排列 全组合

全排列算法是一种比较常考的算法,他的做法也比较多样. 首先我们来看看最符合我们直观思考的,思路是这样的:假如没有重复元素时,传入一个数组A,并插入到另外一个数组B中,假如B中已经包含这个元素,则跳过,否则插入数组B.我们来看看具体代码: <span style="font-size:14px;">public static void permutation1(final String str, String buffer){ if (str.length() == buff

高效率的全组合算法(Java版实现)

博客上看到的一个算法,用Java实现了一个 算法描述: 算法说明:当n大于2时,n个数的全组合一共有(2^n)-1种. 当对n个元素进行全组合的时候,可以用一个n位的二进制数表示取法. 1表示在该位取,0表示不取.例如,对ABC三个元素进行全组合,  100表示取A,010表示取B,001表示取C,101表示取AC  110表示取AB,011表示取BC,111表示取ABC 注意到表示取法的二进制数其实就是从1到7的十进制数 推广到对n个元素进行全排列,取法就是从1到2^n-1的所有二进制形式 要

字符串的全组合

大家千万不要把全组合和全排列搞混了,给定一个字符串:abc, 全组合形式:a,b,c,ab,ac,bc,abc 全排列形式:abc,acb,bac,bca,cab,cba 下面我就讲一下全组合问题. 首先讲一下组合问题原理: n个元素选m个元素的组合问题的实现. 从后往前选取, 选定位置i后, 再在前i-1个里面选取m-1个. * 如: 1, 2, 3, 4, 5 中选取3个元素. * 1) 选取5后, 再在前4个里面选取2个, 而前4个里面选取2个又是一个子问题, 递归即可; * 2) 如果不

搜索入门练习题3 全组合 题解

题目出处:<信息学奥赛一本通>例5.2. 题目描述 设有 \(n\) 个数的集合 \(\{1,2,...,n\}\) ,从中任意取出 \(r\) 个数进行排列 \((r \le n)\) ,试列出所有的排列. 输入格式 输入包含两个正数 \(n,r(1 \le r \le n \le 10)\) 输出格式 输出从 \(n\) 个数的集合中选出 \(r\) 个数的所有组合,每个组合方案占一行.对于每个组合,按照从小到大的顺序输出组合中的所有元素,两两之间有一个空格分隔. 样例输入 3 2 样例输

算法:全组合算法

public static List<int[]> combin(final int TOTAL, final int SELETED) { List<int[]> list = new ArrayList<int[]>(400000); int[] i = new int[SELETED]; for (int x = 1; x <= SELETED; x++) i[x - 1] = x; final int LAST = SELETED - 1; while (

递归求解几类排列组合问题(二、全组合排列)

对于搜索的深度很深或深度不固定的情况,则无法用枚举的方法来设置循环嵌套的层数,这时可以考虑用递归法来完成搜索任务.递归是一种常用算法,它是搜索的另一种实现方式.如果在算法设计中采用一个函数或过程直接或间接地调用它自身来解决问题的方法,则称该方法为递归算法.递归算法必须要设计好一个或若干个确定的递归终止条件. Sample Input 3 1 2 3 Sample Output 123 132 213 231 312 321 #include<stdio.h> #include<strin

全组合算法

//全组合算法        public static List<List<T>> FullCombination<T>(List<T> lstSource)        {            var n = lstSource.Count;            var max = 1 << n;            var lstResult = new List<List<T>>();