1. 程式人生 > >全排列和全組合實現

全排列和全組合實現

.html 有意義 per more tro 包含 方法 循環 -s

記得@老趙之前在微博上吐槽說,“有的人真是毫無長進,六年前某同事不會寫程序輸出全排列,昨天發郵件還是問我該怎麽寫,這時間浪費到我都看不下去了。” 那時候就很好奇全排列到底是什麽東西,到底有多難? 今天復習的時候終於碰到這題了,結果果然自己太渣,看了好久都沒明白,代碼實現又是磕磕碰碰的。所以,就把它整理成筆記加深記憶,也希望能幫到和我一樣的人。 全排列 所謂全排列,就是打印出字符串中所有字符的所有排列。例如輸入字符串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);
}

參考資料
  • MoreWindows-STL系列之十 全排列
  • java 全組合 與全排列
本文大部分內容源自:http://wuchong.me/blog/2014/07/28/permutation-and-combination-realize/

全排列和全組合實現