輸入乙個字串,列印出該字串中字元的所有排列。例如輸入字串abc,則輸出由字元a、b、c 所能排列出來的所有字串abc, acb, bac, bca, cab, cba。
又是乙個經典問題,最容易想到的解決方法仍然是窮舉(我實在是太愛窮舉法了,每當被問到演算法問題不知道如何解決的時候,總可以祭出窮舉大旗,從而多爭取3分鐘的思考時間
在這種情況下,窮舉法裹著貂皮大衣的親戚——遞迴就出現了。雖然空間複雜度和時間複雜度沒有任何改進,而且還增加了系統開銷(關於遞迴法的系統開銷不在這裡討論,之後再找專門的時間闡述),但是就是因為長得好看(**看起來精煉),遞迴的b格兒就高了很多。
遞迴法對於這個題目同樣非常適用,基本思路就是固定乙個字元,然後對剩餘的字元做全排列……不贅述,請自己想。如果你也跟我一樣永遠想不明白遞迴,那就畫畫圖,寫寫**,debug一下,每天花3-4個小時,靜下心來仔細捉摸,總(ye)會(bu)想(hui)明白的。貼一段july和他夥伴們在《程式設計師程式設計藝術:面試和演算法心得》中的**實現,供做噩夢時使用。p.s. 我已加了注釋
/*
* permute full array of input string by general recusion
* @ char* perm [in/out] the string need to do permutation
* @ int from [in] the start position of the string
* @ int to [in] the end position of the string
*/void calcallpermutation(char* perm, int from, int to)
if (from == to)
else}}
這是乙個比遞迴更有趣的答案,不知道算不算經典解法,起碼開拓了思路,跟每一次接觸新鮮的演算法一樣,仍然想了半天的時間,因此照例把思考過程更細緻的記錄下來(雖然july和他夥伴們在《程式設計師程式設計藝術:面試和演算法心得》中已經說了很多),再加上一些小修改。
字典序,顧名思義,就是按照字典的順序進行排列。根據維基百科的定義:給定兩個偏序集a和b,(a,b)和(a′,b′)屬於笛卡爾集 a × b,則字典序定義為(a,b) ≤ (a′,b′) 當且僅當 a < a′ 或 (a = a′ 且 b ≤ b′)。所以給定兩個字串,逐個字元比較,那麼先出現較小字元的那個串字典順序小,如果字元一直相等,較短的串字典順序小。例如:abc < abcd < abde < afab。
總結一下,字典序排序其實就是同一組字元組成的一系列字串,
起點: 字典序最小的排列, 1~n , 例如12345
終點: 字典序最大的排列,n~1, 例如54321
過程: 從當前字串排列生成字典序剛好比它大的下乙個排列,比如12345的字典序下乙個排列是12354
現在來進一步分析一下演算法實現,其實對於字典序排列,關鍵點就是找到「下乙個排列」。基本的查詢方法
假定現有字串(a)x現在的問題是如何找到「x」和「y」?
舉個例子,現在我們要找21543的下乙個排列,我們可以從左至右逐個掃瞄每個數,看哪個能增大(至於如何判定能增大,是根據如果乙個數右面有比它大的數存在,那麼這個數就能增大),我們可以看到最後乙個能增大的數是:x = 1。而1應該增大到多少?1能增大到它右面比它大的那一系列數中最小的那個數,即:y = 3,故此時21543的下乙個排列應該變為23***,顯然 ***(對應之前的b』)應由小到大排,於是我們最終找到比「21543」大,但字典順序盡量小的23145,找到的23145剛好比21543大。
抽象概括一下上面的例子就是「二找、一交換、一翻轉」。
一找:找到排列中最後(最右)乙個公升序的首位位置i,x = ai
二找:找到排列中第i位右邊最後乙個比ai 大的位置j,y = aj
一交換:交換x,y
一翻**把第(i+ 1)位到最後的部分翻轉
*公升序:相鄰兩個位置ai < ai+1,ai 稱作該公升序的首位
找21543的下乙個排列,套用「二找、一交換、一翻轉」就是
一找: 找到排列中最後(最右)乙個公升序的首位, x = 1
二找: 找到排列中1的右邊最後乙個比1大的位置,y = 3
一交換:1和3交換,得23541
一翻**翻轉541,得23145
道理講完了,但是你真的懂了嗎?反正本人看到這裡又看了演算法之後,仍然懵懵懂懂(請原諒我的智商吧,其實我自己也挺著急的,媽媽已經急哭,表示對我放棄**了
首先來看第一找「找到排列中最後(最右)乙個公升序的首位位置i,x = ai」
這意味著什麼?這意味著字串在x之後所有字元都是降序排列的,如下圖。在找到x=1之前,543已經達到了最大字典序,因此不可能通過改變543的順序得到更大的字串。
那麼為什麼不是修改首位的「2」呢?還記得前面介紹的字串(a)x(b)的下乙個排列是(a)y(b』)的方法嗎?,a要盡可能長。對「1」進行操作正式保證了a盡可能長。
接下來,看一下第二找和一交換:「找到排列中第i位右邊最後乙個比ai 大的位置j,y = aj」,「交換x,y」
說完了「a要盡可能長」,現在該說y要盡可能小了。為什麼「第二找」和「一交換」之後,y就最小了呢?既然首位的「2」是不在範圍內的,而「1」(即x)是要被交換的,那麼y只能來自「5」,「4」,'3「,因為543是降序排列的(注意,x可是找到的字串中最後乙個公升序的首位喲),因此「5」,「4」,'3「中比」1「(即x)大的最小的字元就是y。於是y=3。交換x,y之後,即得到新的字串(a)y(b')
此時的b'仍然不是我們最終需要的b',因為它還不滿足最後乙個條件b'裡的字元按由小到大遞增排列。為了做到這一點,於是有了最後的「一翻轉」。那麼為什麼簡單的翻轉之後b'裡的字元就按照由小到大的順序排列了呢?
我們在來回顧一下b和b『的確定過程。首先,b是乙個降序排列的字串;然後我們在b中找到了比x小的最小的y(此處有些繞,自己寫幾個字串就一目了然了),也就是說y的右側如果還有字元的話也一定比x小(因為b是降序),接下來交換x和y得到b',因此b』也是降序的。對於乙個降序的字串來說,翻轉之後即為公升序排列。於是我們得到了最終的(a)y(b'),即23145。
好了,該講的都講完了,現在看**
/*
* find out the next (bigger) permutation of current string.
* @ char* perm [in/out] string
* @ int num [in] the length of string.
*/bool findnextpermutation(char* perm, int num)
int j = 0;
for(j = num - 1; (j > i) && perm[j] <= perm[i]; --j)
; // find y
swap(perm[i], perm[j]); // swap x and y
reverse(perm + i + 1, perm + num); // reverse b'
return true;
}
這段**實現了從當前字串生成字典序剛好比它大的下乙個排列,但是如果我們拿到的字串不是字典序最小的排列,該如何處理呢?兩種方法
方法一:
對原始字串進行排序,將原始字串轉換為字典序最小的排列後,再通過字典序排序進行全排列。這樣做的好處是實現簡單,缺點是要多做一次字串排序。關於排序演算法不在本文討論範圍,這裡我直接使用了stl的sort函式
void calcbydictionary(const string &str)
cout<= 0; --i)
; if(i < 0)
int j = 0;
for(j = num - 1; (j > i) && perm[j] >= perm[i]; --j)
; swap(perm[i], perm[j]);
reverse(perm + i + 1, perm + num);
return true;
}
最近一直在看july和他夥伴們的《程式設計師程式設計藝術:面試和演算法心得》,收穫良多。在學習的過程中發現,雖然原書講解的很細緻,但是真正理解仍然需要花費大量的思考和實踐。因此做了這個系列的文章,只是希望將自己的思考記錄下來,供以後查閱。 全排列演算法的字典序排列
之前在中描述了全排列演算法的遞迴解法,這裡再說一種演算法 字典序排列。字典序排列就是按照字典a z,1 9的順序給出字串的順序全排列,例如abc的全排列就是從abc一直排到cba。那麼給定乙個字串,怎麼找出恰好大於該字串的下乙個排列呢?我們考慮如下的步驟 1 假設字串為p1p2 pn,我們從後往前尋...
陣列,字串全排列演算法分析(字典序生成法)
先看乙個題 題目描述 輸入乙個字串,按字典序列印出該字串中字元的所有排列。例如輸入字串abc,則列印出由字元a,b,c所能排列出來的所有字串abc,acb,bac,bca,cab和cba。結果請按字母順序輸出。輸入描述 輸入乙個字串,長度不超過9 可能有字元重複 字元只包括大小寫字母。就是輸入乙個字...
演算法練習 字串包含
給定兩個分別由字母組成的字串a和字串b,字串b的長度比字串a短。請問,如何最快地判斷字串b中所有字母是否都在字串a裡?為了簡單起見,我們規定輸入的字串只包含大寫英文本母,請實現函式bool stringcontains string a,string b 比如,如果是下面兩個字串 string 1 ...