字符串的全排列(字典序排列)
题目描述
输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串abc, acb, bac, bca, cab, cba。
题目分析
穷举与递归
又是一个经典问题,最容易想到的解决方法仍然是穷举(我实在是太爱穷举法了,每当被问到算法问题不知道如何解决的时候,总可以祭出穷举大旗,从而多争取3分钟的思考时间)。穷举虽好,但它大多数情况下都不是被需要的那个答案,是因为看起来代码太Low不够高大上吗?
在这种情况下,穷举法裹着貂皮大衣的亲戚——递归就出现了。虽然空间复杂度和时间复杂度没有任何改进,而且还增加了系统开销(关于递归法的系统开销不在这里讨论,之后再找专门的时间阐述),但是就是因为长得好看(代码看起来精炼),递归的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 (to <= 1){return;}if (from == to){//all characters has been permutedfor (int i = 0; i <= to; i++)cout << perm[i];cout << endl;}else{// always select one character, then full array the left ones.for (int j = from; j <= to; j++){ swap(perm[j], perm[from]); //swap the selected character to the beginning of stringCalcAllPermutation(perm, from + 1, to); // Permute left characters in full array.swap(perm[j], perm[from]); //recovery the string to original one (swap the selected character back to its position.) }} }
字典序
这是一个比递归更有趣的答案,不知道算不算经典解法,起码开拓了思路,跟每一次接触新鲜的算法一样,仍然想了半天的时间,因此照例把思考过程更细致的记录下来(虽然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(B),它的下一个排列是:(A)y(B’),其中A、B和B’是“字符串”(可能为空),x和y是“字符”,前缀相同,都是A,且一定有y > x。那么,为使下一个排列字典顺序尽可能小,必有:
- A尽可能长 (A越长,那么B‘就越短,从而y所在的位越低,很明显的同一个字符放在低位肯定比放在高位要小,比如:100<10< 1, abc<aac<aaa)
- y尽可能小 (同一个位置上,字符越小整个字符串的字典序越小,比如:131<121, acd<abc)
- B'里的字符按由小到大递增排列 (小朋友都懂的道理不解释)
现在的问题是如何找到“x”和“y”?
举个例子,现在我们要找21543的下一个排列,我们可以从左至右逐个扫描每个数,看哪个能增大(至于如何判定能增大,是根据如果一个数右面有比它大的数存在,那么这个数就能增大),我们可以看到最后一个能增大的数是:x = 1。而1应该增大到多少?1能增大到它右面比它大的那一系列数中最小的那个数,即:y = 3,故此时21543的下一个排列应该变为23xxx,显然 xxx(对应之前的B’)应由小到大排,于是我们最终找到比“21543”大,但字典顺序尽量小的23145,找到的23145刚好比21543大。
抽象概括一下上面的例子就是“二找、一交换、一翻转”。
*升序:相邻两个位置ai < ai+1,ai 称作该升序的首位
找21543的下一个排列,套用“二找、一交换、一翻转”就是
道理讲完了,但是你真的懂了吗?反正本人看到这里又看了算法之后,仍然懵懵懂懂(请原谅我的智商吧,其实我自己也挺着急的,妈妈已经急哭,表示对我放弃治疗了)。因此,下面的部分很重要。
首先来看第一找“找到排列中最后(最右)一个升序的首位位置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 i = 0;for(i = num - 2; (perm[i] >= perm[i+1]) && i >= 0; --i); // Find xif(i < 0){return false; // The input string is a single character }int j = 0;for(j = num - 1; (j > i) && perm[j] <= perm[i]; --j); // Find y swap(perm[i], perm[j]); // swap x and yreverse(perm + i + 1, perm + num); // reverse B'return true; }
这段代码实现了从当前字符串生成字典序刚好比它大的下一个排列,但是如果我们拿到的字符串不是字典序最小的排列,该如何处理呢?
对原始字符串进行排序,将原始字符串转换为字典序最小的排列后,再通过字典序排序进行全排列。这样做的好处是实现简单,缺点是要多做一次字符串排序。关于排序算法不在本文讨论范围,这里我直接使用了STL的sort函数
void CalcByDictionary(const string &str) {char* perm = const_cast<char*>(str.c_str());sort(perm, perm+str.size());cout<<str<<endl;while(true){if(!FindNextPermutation(perm, str.size())){break;}cout<<s<<endl;} }
题外话
最近一直在看July和他伙伴们的《程序员编程艺术:面试和算法心得》,收获良多。在学习的过程中发现,虽然原书讲解的很细致,但是真正理解仍然需要花费大量的思考和实践。因此做了这个系列的文章,只是希望将自己的思考记录下来,供以后查阅。
转载于:https://www.cnblogs.com/chailinbo/p/9269210.html
总结
以上是生活随笔为你收集整理的字符串的全排列(字典序排列)的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 不要再加荐股群了,不然下一个“接盘侠”就
- 下一篇: Scrapy框架----pipeline