字符串的全排列

字符串的全排列

題目描述:

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

例如輸入字符串abc,則輸出由字符a、b、c 所能排列出來的所有字符串:
abc、acb、bac、bca、cab 和 cba。

分析和解法:

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

  • 必須有可達到的終止條件,否則程序陷入死循環
  • 子問題在規模上比原問題小
  • 子問題可通過再次遞歸調用求解
  • 子問題的解應能組合成整個問題的解

解法一:遞歸實現

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

  • 首先,我們固定第一個字符a,求后面兩個字符bc的排列
  • 當兩個字符bc排列求好之后,我們把第一個字符a和后面的b交換,得到bac,接著我們固定第一個字符b,求后面兩個字符ac的排列
  • 現在是把c放在第一個位置的時候了,但是記住前面我們已經把原先的第一個字符a和后面的b做了交換,為了保證這次c仍是和原先處在第一個位置的a交換,我們在拿c和第一個字符交換之前,先要把b和a交換回來。在交換b和a之后,再拿c和處于第一位置的a進行交換,得到cba。我們再次固定第一個字符c,求后面兩個字符b、a的排列
  • 既然我們已經知道怎么求三個字符的排列,那么固定第一個字符之后求后面兩個字符的排列,就是典型的遞歸思路了

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


源代碼如下:

#include <iostream>
#include <cstring>

using namespace std;

void AllPermutation(char* perm, int from, int to)
{
    if(from > to)
        return;
            
    if(from == to)     //打印當前排列 
    {
        static int count = 1;    //局部靜態變量,用來統計全排列的個數  
        cout << count++ << ":" << perm;
        cout << endl;
    }
    if(from < to)     //用遞歸實現全排列 
    {
        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;
}

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

由于全排列就是從第一個數字起,每個數分別與它后面的數字交換,我們先嘗試加個這樣的判斷——如果一個數與后面的數字相同那么這兩個數就不交換了。例如abb,第一個數與后面兩個數交換得bab,bba。然后abb中第二個數和第三個數相同,就不用交換了。但是對bab,第二個數和第三個數不同,則需要交換,得到bba。由于這里的bba和開始第一個數與第三個數交換的結果相同了,因此這個方法不行。

換種思維,對abb,第一個數a與第二個數b交換得到bab,然后考慮第一個數與第三個數交換,此時由于第三個數等于第二個數,所以第一個數就不再用與第三個數交換了。再考慮bab,它的第二個數與第三個數交換可以解決bba。此時全排列生成完畢!

這樣,我們得到在全排列中去掉重復的規則:

去重的全排列就是從第一個數字起,每個數分別與它后面非重復出現的數字交換。

源代碼如下:

#include <iostream>
#include <cstring>

using namespace std;

//在[from, to]區間中是否有字符與下標為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)     //打印當前排列 
    {
        static int count = 1;    //局部靜態變量,用來統計全排列的個數  
        cout << count++ << ":" << perm;
        cout << endl;
    }
    if(from < to)     //用遞歸實現全排列 
    {
        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;
}

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

解法二:字典序排列

首先,咱們得清楚什么是字典序。根據維基百科的定義:給定兩個偏序集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
  • 過程: 從當前排列生成字典序剛好比它大的下一個排列

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

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

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

現在的問題是:找到x和y。怎么找到呢?咱們來看一個例子。

比如說,現在我們要找21543的下一個排列,我們可以從左至右逐個掃描每個數,看哪個能增大(至于如何判定能增大,是根據如果一個數右面有比它大的數存在,那么這個數就能增大),我們可以看到最后一個能增大的數是:x = 1。而1應該增大到多少?1能增大到它右面比它大的那一系列數中最小的那個數,即:y = 3,故此時21543的下一個排列應該變為23xxx,顯然 xxx(對應之前的B’)應由小到大排,于是我們最終找到比“21543”大,但字典順序盡量小的23145,找到的23145剛好比21543大。

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

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

還是拿上面的21543舉例,那么,應用next_permutation算法的過程如下:

  • x = 1;
  • y = 3
  • 1和3交換,得23541
  • 翻轉541,得23145

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

源代碼如下:

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

using namespace std;

//反轉區間
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)    //找升序的相鄰兩數,前一個數即替換數
        {
            //從后向前找比替換點大的第一個數
            pFind = pEnd;
            while(*pFind <= *p) 
                --pFind;
            swap(*p, *pFind);
            //替換點后的數全部反轉
            Reverse(q, pEnd);
            return true; 
        } 
    }
    //如果沒有找到下一個排列,全部反轉后返回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;
}

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

特別注意:

  • 一定要注意邊界條件和判斷條件,到底是 > 還是 >= ,會影響結果。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容

  • 一、深搜法 二、遞歸法 對于包含重復字符的字符串的全排列,在使用遞歸法交換兩個元素之前,需要判斷是否需要交換。在f...
    鬼谷神奇閱讀 2,117評論 0 0
  • 輸入一個字符串,打印出該字符串中字符的所有排列 方法一:遞歸實現 遞歸的實現思想是固定一位,對剩下的字符串實現全排...
    雨_樹閱讀 369評論 0 0
  • 題目:輸入一個字符串,打印出該字符串中字符的所有排列。例如輸入字符串abc,則輸出由字符a,b,c所能排列出來的所...
    FlyElephant閱讀 912評論 0 0
  • 1. 排列 鏈接注意字符重復與非重復兩種情況的區別。非遞歸實現有點麻煩 2. 組合 2.1 什么是組合 有abc得...
    yangqi916閱讀 925評論 0 1
  • 從醫學的角度來說,晚上晚飯時間之后,沒有多久,人就會休息,代謝會比較緩慢,這個時候,如果有太多的食物在胃中,會對胃...
    Hanc_閱讀 466評論 0 1