記得@老趙之前在微博上吐槽說,「有的人真是毫無長進,六年前某同事不會寫程式輸出全排列,昨天發郵件還是問我該怎麼寫,這時間浪費到我都看不下去了。」 那時候就很好奇全排列到底是什麼東西,到底有多難?
今天覆習的時候終於碰到這題了,結果果然自己太渣,看了好久都沒明白,**實現又是磕磕碰碰的。所以,就把它整理成筆記加深記憶,也希望能幫到和我一樣的人。
全排列所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串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(instring) (ret string
) fmt.printf(
"%v\n
", reslut)
for i := 1; i < num; i++ ) }}
reslut =midrelsut
}fmt.printf(
"*****%v\n
", reslut)
//轉換為字串
for _, cur :=range reslut
return
}
func main()
首先考慮bac和cba這二個字串是如何得出的。顯然這二個都是abc中的 a 與後面兩字元交換得到的。然後可以將abc的第二個字元和第三個字元交換得到acb。同理可以根據bac和cba來得bca和cab。
因此可以知道 全排列就是從第乙個數字起每個數分別與它後面的數字交換,也可以得出這種解法每次得到的結果都是正確結果,所以複雜度為 o(n!)。找到這個規律後,遞迴的**就很容易寫出來了:
#include#include//交換兩個字元
void swap(char *a ,char *b)
//遞迴全排列,start 為全排列開始的下標, length 為str陣列的長度
void allrange(char* str,int start,int
length)
else
}}void permutation(char*str)
void
main()
去重的全排列
為了得到不一樣的排列,可能我們最先想到的方法是當遇到和自己相同的就不交換了。如果我們輸入的是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#include//交換兩個字元
void swap(char *a ,char *b)
//在 str 陣列中,[start,end) 中是否有與 str[end] 元素相同的
bool isswap(char* str,int start,int
end)
return
true;}
//遞迴去重全排列,start 為全排列開始的下標, length 為str陣列的長度
void allrange2(char* str,int start,int
length)
else}}
}void permutation(char*str)
void
main()
全組合如果不是求字元的所有排列,而是求字元的所有組合應該怎麼辦呢?還是輸入三個字元 a、b、c,則它們的組合有a b c ab ac bc abc。當然我們還是可以借鑑全排列的思路,利用問題分解的思路,最終用遞迴解決。不過這裡介紹一種比較巧妙的思路 —— 基於位圖。
假設原有元素 n 個,則最終組合結果是 2^n−1 個。我們可以用位操作方法:假設元素原本有:a,b,c 三個,則 1 表示取該元素,0 表示不取。故取a則是001,取ab則是011。所以一共三位,每個位上有兩個選擇 0 和 1。而000沒有意義,所以是2n−1個結果。
這些結果的位圖值都是 1,2…2^n-1。所以從值 1 到值 2^n−1 依次輸出結果:
001,010,011,100,101,110,111 。對應輸出組合結果為:a,b,ab,c,ac,bc,abc。
因此可以迴圈 1~2^n-1,然後輸出對應代表的組合即可。有**如下:
#include#includevoid combination(char *str)
}printf("\n
");}
}void
main()
參考資料
本文大部分內容源自:
全排列和全組合實現
所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串abc,則列印出 a b c 所能排列出來的所有字串abc acb bac bca cab和cba。一般最先想到的方法是暴力迴圈法,即對於每一位,遍歷集合中可能的元素,如果在這一位之前出現過了該元素,跳過該元素。例如對於abc,第一位可以是...
全排列和全組合
所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串abc,則列印出 a b c 所能排列出來的所有字串abc acb bac bca cab和cba。一般最先想到的方法是暴力迴圈法,即對於每一位,遍歷集合中可能的元素,如果在這一位之前出現過了該元素,跳過該元素。例如對於abc,第一位可以是...
全排列和全組合的實現
所謂全排列,就是列印出字串中所有字元的所有排列。例如輸入字串abc,則列印出 a b c 所能排列出來的所有字串abc acb bac bca cab和cba。一般最先想到的方法是暴力迴圈法,即對於每一位,遍歷集合中可能的元素,如果在這一位之前出現過了該元素,跳過該元素。例如對於abc,第一位可以是...