字符串的全排列

字符串的全排列

題目描述:

輸入一個字符串,打印出該字符串中字符的所有排列园蝠。

例如輸入字符串a(chǎn)bc渺蒿,則輸出由字符a、b彪薛、c 所能排列出來的所有字符串:
abc茂装、acb、bac善延、bca少态、cab 和 cba。

分析和解法:

這是典型的遞歸求解問題易遣,遞歸算法有四個特性:

  • 必須有可達到的終止條件彼妻,否則程序陷入死循環(huán)
  • 子問題在規(guī)模上比原問題小
  • 子問題可通過再次遞歸調(diào)用求解
  • 子問題的解應(yīng)能組合成整個問題的解

解法一:遞歸實現(xiàn)

對于字符串的排列問題:
如果能生成n-1個元素的全排列,就能生成n個元素的全排列豆茫。對于只有一個元素的集合澳骤,可以直接生成全排列。所以全排列的遞歸終止條件很明確澜薄,只有一個元素時为肮。我們可以分析一下全排列的過程:

  • 首先,我們固定第一個字符a肤京,求后面兩個字符bc的排列
  • 當(dāng)兩個字符bc排列求好之后颊艳,我們把第一個字符a和后面的b交換,得到bac忘分,接著我們固定第一個字符b棋枕,求后面兩個字符ac的排列
  • 現(xiàn)在是把c放在第一個位置的時候了,但是記住前面我們已經(jīng)把原先的第一個字符a和后面的b做了交換妒峦,為了保證這次c仍是和原先處在第一個位置的a交換重斑,我們在拿c和第一個字符交換之前,先要把b和a交換回來肯骇。在交換b和a之后窥浪,再拿c和處于第一位置的a進行交換,得到cba笛丙。我們再次固定第一個字符c漾脂,求后面兩個字符b、a的排列
  • 既然我們已經(jīng)知道怎么求三個字符的排列胚鸯,那么固定第一個字符之后求后面兩個字符的排列骨稿,就是典型的遞歸思路了

下面這張圖很清楚的給出了遞歸的過程:


源代碼如下:

#include <iostream>
#include <cstring>

using namespace std;

void AllPermutation(char* perm, int from, int to)
{
    if(from > to)
        return;
            
    if(from == to)     //打印當(dāng)前排列 
    {
        static int count = 1;    //局部靜態(tài)變量,用來統(tǒng)計全排列的個數(shù)  
        cout << count++ << ":" << perm;
        cout << endl;
    }
    if(from < to)     //用遞歸實現(xiàn)全排列 
    {
        for(int j = from; j <= to; j++)    //第j個字符分別與它后面的字符交換就能得到新的排列
        {
                swap(perm[j], perm[from]);
            //cout<<0;
            AllPermutation(perm, from + 1, to);
            //cout<<1;
            swap(perm[j], perm[from]);
            //cout<<2;
            
        }
    }
}

int main()
{
    char str[100];
    cin >> str;
    AllPermutation(str, 0, strlen(str) - 1);
    return 0;
}

但是如果輸入里有重復(fù)字符又該如何去掉呢?

由于全排列就是從第一個數(shù)字起坦冠,每個數(shù)分別與它后面的數(shù)字交換形耗,我們先嘗試加個這樣的判斷——如果一個數(shù)與后面的數(shù)字相同那么這兩個數(shù)就不交換了。例如abb辙浑,第一個數(shù)與后面兩個數(shù)交換得bab激涤,bba。然后abb中第二個數(shù)和第三個數(shù)相同例衍,就不用交換了。但是對bab已卸,第二個數(shù)和第三個數(shù)不同佛玄,則需要交換,得到bba累澡。由于這里的bba和開始第一個數(shù)與第三個數(shù)交換的結(jié)果相同了梦抢,因此這個方法不行。

換種思維愧哟,對abb奥吩,第一個數(shù)a與第二個數(shù)b交換得到bab,然后考慮第一個數(shù)與第三個數(shù)交換蕊梧,此時由于第三個數(shù)等于第二個數(shù)霞赫,所以第一個數(shù)就不再用與第三個數(shù)交換了。再考慮bab肥矢,它的第二個數(shù)與第三個數(shù)交換可以解決bba端衰。此時全排列生成完畢!

這樣甘改,我們得到在全排列中去掉重復(fù)的規(guī)則:

去重的全排列就是從第一個數(shù)字起旅东,每個數(shù)分別與它后面非重復(fù)出現(xiàn)的數(shù)字交換。

源代碼如下:

#include <iostream>
#include <cstring>

using namespace std;

//在[from, to]區(qū)間中是否有字符與下標(biāo)為from的字符相等  
bool IsSwap(char* from, char* to)
{
    char* p;
    for(p = from; p < to; p++)
    {
        if(*p == *to)
            return false;   
    }   
    return true;
} 

void AllPermutation(char* perm, int from, int to)
{
    if(from > to)
        return;
            
    if(from == to)     //打印當(dāng)前排列 
    {
        static int count = 1;    //局部靜態(tài)變量十艾,用來統(tǒng)計全排列的個數(shù)  
        cout << count++ << ":" << perm;
        cout << endl;
    }
    if(from < to)     //用遞歸實現(xiàn)全排列 
    {
        for(int j = from; j <= to; j++)    //第j個字符分別與它后面的字符交換就能得到新的排列
        {
            if(IsSwap((perm + j), (perm + to)))
            {
                swap(perm[j], perm[from]);
                //cout<<0;
                AllPermutation(perm, from + 1, to);
                //cout<<1;
                swap(perm[j], perm[from]);
                //cout<<2;
            }
        }
    }
}

int main()
{
    char str[100];
    cin >> str;
    AllPermutation(str, 0, strlen(str) - 1);
    return 0;
}

分析:時間復(fù)雜度為O(n!)抵代。這個解法不難想到,但是需要注意去除重復(fù)的那塊處理忘嫉,用最后一位與前面的每個字符比較荤牍,如果相等,就不交換庆冕,否則交換参淫。

解法二:字典序排列

首先,咱們得清楚什么是字典序愧杯。根據(jù)維基百科的定義:給定兩個偏序集A和B,(a,b)和(a′,b′)屬于笛卡爾集 A × B涎才,則字典序定義為

(a,b) ≤ (a′,b′) 當(dāng)且僅當(dāng) a < a′ 或 (a = a′ 且 b ≤ b′)。

所以給定兩個字符串,逐個字符比較耍铜,那么先出現(xiàn)較小字符的那個串字典順序小邑闺,如果字符一直相等,較短的串字典順序小棕兼。例如:abc < abcd < abde < afab陡舅。

那有沒有這樣的算法,使得

  • 起點: 字典序最小的排列, 1-n , 例如12345
  • 終點: 字典序最大的排列伴挚,n-1, 例如54321
  • 過程: 從當(dāng)前排列生成字典序剛好比它大的下一個排列

答案是肯定的:有靶衍,即是STL中的next_permutation算法。

在了解next_permutation算法是怎么一個過程之前茎芋,咱們得先來分析下“下一個排列”的性質(zhì)颅眶。

  • 假定現(xiàn)有字符串(A)x(B),它的下一個排列是:(A)y(B’)田弥,其中A涛酗、B和B’是“字符串”(可能為空),x和y是“字符”偷厦,前綴相同商叹,都是A,且一定有y > x只泼。
  • 那么剖笙,為使下一個排列字典順序盡可能小,必有:
    • A盡可能長
    • y盡可能小
    • B’里的字符按由小到大遞增排列

現(xiàn)在的問題是:找到x和y请唱。怎么找到呢枯途?咱們來看一個例子。

比如說籍滴,現(xiàn)在我們要找21543的下一個排列酪夷,我們可以從左至右逐個掃描每個數(shù),看哪個能增大(至于如何判定能增大孽惰,是根據(jù)如果一個數(shù)右面有比它大的數(shù)存在晚岭,那么這個數(shù)就能增大),我們可以看到最后一個能增大的數(shù)是:x = 1勋功。而1應(yīng)該增大到多少坦报?1能增大到它右面比它大的那一系列數(shù)中最小的那個數(shù),即:y = 3狂鞋,故此時21543的下一個排列應(yīng)該變?yōu)?3xxx片择,顯然 xxx(對應(yīng)之前的B’)應(yīng)由小到大排,于是我們最終找到比“21543”大骚揍,但字典順序盡量小的23145字管,找到的23145剛好比21543大啰挪。

由這個例子可以得出next_permutation算法流程為:
next_permutation算法

  • 定義
    • 升序:相鄰兩個位置ai < ai+1,ai 稱作該升序的首位
  • 步驟(二找嘲叔、一交換亡呵、一翻轉(zhuǎn))
    • 找到排列中最后(最右)一個升序的首位位置i,x = ai
    • 找到排列中第i位右邊最后一個比ai 大的位置j硫戈,y = aj
    • 交換x锰什,y
    • 把第(i+ 1)位到最后的部分翻轉(zhuǎn)

還是拿上面的21543舉例,那么丁逝,應(yīng)用next_permutation算法的過程如下:

  • x = 1汁胆;
  • y = 3
  • 1和3交換,得23541
  • 翻轉(zhuǎn)541霜幼,得23145

23145即為所求的21543的下一個排列嫩码。

源代碼如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cassert>

using namespace std;

//反轉(zhuǎn)區(qū)間
void Reverse(char* begin, char* end)
{
    while(begin < end)
        swap(*begin++, *end--);
}  

//下一個排列
bool NextPermutation(char* str)
{
    assert(str);    //檢查空指針
    
    char *p, *q, *pFind;
    char *pEnd = str + strlen(str) - 1;
    if(str == pEnd)
        return false;
    p = pEnd;
    while(p != str)
    {
        q = p;
        p--;
        if(*p < *q)    //找升序的相鄰兩數(shù),前一個數(shù)即替換數(shù)
        {
            //從后向前找比替換點大的第一個數(shù)
            pFind = pEnd;
            while(*pFind <= *p) 
                --pFind;
            swap(*p, *pFind);
            //替換點后的數(shù)全部反轉(zhuǎn)
            Reverse(q, pEnd);
            return true; 
        } 
    }
    //如果沒有找到下一個排列辛掠,全部反轉(zhuǎn)后返回false
    Reverse(str, pEnd);   
    return false; 
}

int cmp(const void *a,const void *b)  
{  
    return int(*(char *)a - *(char *)b);  
}

int main()
{
    char str[100];
    cin >> str;
    int count = 1;
    qsort(str, strlen(str), sizeof(char), cmp);
    do
    {
        cout << count++ << ":" << str << endl;
    }while(NextPermutation(str));
    return 0;
}

分析: 時間復(fù)雜度為O(n!)谢谦。這個版本是可以有重復(fù)字符的释牺。

特別注意:

  • 一定要注意邊界條件和判斷條件萝衩,到底是 > 還是 >= ,會影響結(jié)果没咙。

參考資料:《編程之法》The Art of Programming By July
字符串的全排列和組合算法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猩谊,一起剝皮案震驚了整個濱河市鳖轰,隨后出現(xiàn)的幾起案子记餐,更是在濱河造成了極大的恐慌,老刑警劉巖赡磅,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涡驮,死亡現(xiàn)場離奇詭異暗甥,居然都是意外死亡,警方通過查閱死者的電腦和手機捉捅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門撤防,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人棒口,你說我怎么就攤上這事寄月。” “怎么了无牵?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵漾肮,是天一觀的道長。 經(jīng)常有香客問我茎毁,道長克懊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮保檐,結(jié)果婚禮上耕蝉,老公的妹妹穿的比我還像新娘。我一直安慰自己夜只,他們只是感情好垒在,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扔亥,像睡著了一般场躯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旅挤,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天踢关,我揣著相機與錄音,去河邊找鬼粘茄。 笑死签舞,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柒瓣。 我是一名探鬼主播儒搭,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芙贫!你這毒婦竟也來了搂鲫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤磺平,失蹤者是張志新(化名)和其女友劉穎魂仍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拣挪,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡擦酌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了菠劝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赊舶。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖闸英,靈堂內(nèi)的尸體忽然破棺而出锯岖,到底是詐尸還是另有隱情,我是刑警寧澤甫何,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布出吹,位于F島的核電站,受9級特大地震影響辙喂,放射性物質(zhì)發(fā)生泄漏捶牢。R本人自食惡果不足惜鸠珠,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秋麸。 院中可真熱鬧渐排,春花似錦、人聲如沸灸蟆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炒考。三九已至可缚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斋枢,已是汗流浹背帘靡。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓤帚,地道東北人描姚。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像戈次,于是被迫代替她去往敵國和親轩勘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 一、深搜法 二观游、遞歸法 對于包含重復(fù)字符的字符串的全排列搂捧,在使用遞歸法交換兩個元素之前,需要判斷是否需要交換懂缕。在f...
    鬼谷神奇閱讀 2,091評論 0 0
  • 輸入一個字符串允跑,打印出該字符串中字符的所有排列 方法一:遞歸實現(xiàn) 遞歸的實現(xiàn)思想是固定一位,對剩下的字符串實現(xiàn)全排...
    雨_樹閱讀 363評論 0 0
  • 題目:輸入一個字符串搪柑,打印出該字符串中字符的所有排列聋丝。例如輸入字符串a(chǎn)bc,則輸出由字符a工碾,b弱睦,c所能排列出來的所...
    FlyElephant閱讀 883評論 0 0
  • 1. 排列 鏈接注意字符重復(fù)與非重復(fù)兩種情況的區(qū)別。非遞歸實現(xiàn)有點麻煩 2. 組合 2.1 什么是組合 有abc得...
    yangqi916閱讀 903評論 0 1
  • 從醫(yī)學(xué)的角度來說垒拢,晚上晚飯時間之后,沒有多久火惊,人就會休息求类,代謝會比較緩慢,這個時候屹耐,如果有太多的食物在胃中尸疆,會對胃...
    Hanc_閱讀 453評論 0 1