全排列和全組合實現

 
記得 @老趙以前在微博上吐槽說,「有的人真是毫無長進,六年前某同事不會寫程序輸出全排列,昨天發郵件仍是問我該怎麼寫,這時間浪費到我都看不下去了。」 那時候就很好奇全排列究竟是什麼東西,到底有多難?
今天覆習的時候終於碰到這題了,結果果真本身太渣,看了很久都沒明白,代碼實現又是磕磕碰碰的。因此,就把它整理成筆記加深記憶,也但願能幫到和我同樣的人。
全排列
所謂全排列,就是打印出字符串中全部字符的全部排列。例如輸入字符串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 lihaiping1603@aliyun.com
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/
相關文章
相關標籤/搜索