【编程题目】字符串的排列(字符串)★
53.字符串的排列(字符串)。
题目:输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串 abc,则输出由字符 a、b、c 所能排列出来的所有字符串
abc、acb、bac、bca、cab 和 cba。
这道题花了我一天,要好好总结!
思路:这道题目感觉有些难,主要是字符串中的字符可能会有重复。我的想法是把一共有多少种字符和每种字符出现的次数统计出来,每个位置对这些字符变量,下一个位置的可用字符减小,再遍历。
/* 53.字符串的排列(字符串)。 题目:输入一个字符串,打印出该字符串中字符的所有排列。 例如输入字符串 abc,则输出由字符 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba。 */ #include <stdio.h> #include <string.h> #include <stdlib.h> char A[128][2] = {0}; //统计字符串中每种字符出现的次数 char ANS[10000]; //可能的输出 int N; //字符种类数 int LEN; //输入字符串长度 int getTimes(char * in) //统计字符个数 以及 每个字符出现的次数 { int len = strlen(in); bool B[100] = {0}; int n = 0; for(int i = 0; in[i] != '\0'; i++) { if(B[i] == false) { for(int j = i; in[j] != '\0'; j++) { if(in[j] == in[i]) { A[n][0] = in[i]; A[n][1]++; B[j] = true; } } n++; } } return n; } void traverse(int len) //递归求解 每个位置遍历选用A中可用次数大于0的的字符 { if(len == 0) { for(int i = 0; i < LEN; i++) { printf("%c ", ANS[i]); } printf("\n"); return; } for(int i = 0; i < N; i++) { if(A[i][1] != 0) //如果不等于0 说明该字符还可以使用 { ANS[LEN - len] = A[i][0]; //把当前字符存在答案中 A[i][1]--; //当前字符可用次数减1 traverse(len - 1); //求下一个位置的字符 A[i][1]++; //恢复当前字符的可使用次数 } } } void getall(char * in) { LEN = strlen(in); N = getTimes(in); traverse(LEN); } int main() { char in[100] = "abcc"; getall(in); return 0; }
写完后就觉得自己写的不好,好多全局变量,看着难受。
网上看到了很好的解答:http://blog.csdn.net/hackbuteer1/article/details/7462447
给出了递归和非递归的思路。我看完后也仿照着思路重写了一遍,又照着修改了下。
二、去掉重复的全排列的递归实现
由于全排列就是从第一个数字起每个数分别与它后面的数字交换。我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。
换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。
这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。
三、全排列的非递归实现
要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。
如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26
"即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。
对于像“4321”这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。
这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。值得注意的是在循环前要对字符串排序下,可以自己写快速排序的代码
/* http://blog.csdn.net/hackbuteer1/article/details/7462447 在上面的网址上看了答案后 发现自己写的代码实在是太挫了,有很多不必要的全局变量。还有输入字符中一共有几种字符,每种字符的出现次数其实可以不用具体 求出来的。直接比较判断就好了。 网上还给出了非递归算法,找到了从小到大输出所有可能组合的方法,非常值得学习。 */ //递归 #include<stdio.h> #include<string.h> #include <algorithm> using namespace std; bool isswap(char * pBegin, char * pNow) { for(char *p = pBegin; p < pNow; p++) { if(*p == *pNow) return false; } return true; } void traverse(char * pStr,char * pBegin) { if(/**pBegin == '\0'*/strlen(pBegin) == 1) //两种判断方式都可以 { static int n = 0; printf("%d: %s\n", ++n, pStr); } else { for(char *p = pBegin; *p != '\0'; p++) { if(isswap(pBegin, p)) { swap(*pBegin, *p); //注意swap应该是 数值 而不是指针 traverse(pStr, pBegin + 1); swap(*p, *pBegin); } } } } //非递归 int cmp(const void * a,const void * b) { return *((char *)a) - *((char *)b); } void reverse(char * pBegin, char * pEnd) { while(pBegin < pEnd) { swap(*pBegin++, *pEnd--); } } bool nonrtraverse(char * pStr) { int l = strlen(pStr); char * pEnd = pStr + l - 1; //定位最后一个字符 if(pStr == pEnd) //注意对只有一个字符的处理 return false; for(char *p = pEnd; p != pStr; p--) { char * q = p - 1; if(*q < *p) { char* pFind = pEnd; while(*pFind <= *q) //注意这里,找到第一个比交换点数大的数 --pFind; swap(*q, *pFind); reverse(p, pEnd); return true; } } reverse(pStr, pStr +l - 1); return false; } int main() { char str[30] = "1224"; traverse(str, str); qsort(str, strlen(str)-1, sizeof(char), cmp); int i = 0; do { printf("%d:%s\n", ++i, str); } while(nonrtraverse(str)); return 0; }