字符串的全排列

字符串的全排列

题目描述:

输入一个字符串,打印出该字符串中字符的所有排列。

例如输入字符串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阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 一、深搜法 二、递归法 对于包含重复字符的字符串的全排列,在使用递归法交换两个元素之前,需要判断是否需要交换。在f...
    鬼谷神奇阅读 2,084评论 0 0
  • 输入一个字符串,打印出该字符串中字符的所有排列 方法一:递归实现 递归的实现思想是固定一位,对剩下的字符串实现全排...
    雨_树阅读 362评论 0 0
  • 题目:输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a,b,c所能排列出来的所...
    FlyElephant阅读 879评论 0 0
  • 1. 排列 链接注意字符重复与非重复两种情况的区别。非递归实现有点麻烦 2. 组合 2.1 什么是组合 有abc得...
    yangqi916阅读 897评论 0 1
  • 从医学的角度来说,晚上晚饭时间之后,没有多久,人就会休息,代谢会比较缓慢,这个时候,如果有太多的食物在胃中,会对胃...
    Hanc_阅读 453评论 0 1