字符串的全排列
写在前面:本文章为根据七月算法的讲解后的学习笔记与心得。
问题描述
给定一个字符串S[0…N-1],设计算法枚举S的全排列。
1.递归算法
思路:
举个栗子:求字符串s="1234"的全排列,有以下4类情况:
1 -(234);表示以1开头,234做全排列,具体怎么做,先不管(以下皆用此表示方法)
2-(134);表示以2开头,134做全排列
3-(214);表示以3开头,214做全排列
4-(123);表示以4开头,123做全排列
这四种情况合起来,就是1234的全排列,没毛病吧。
同理:以情况一内部为例:234做全排列
又有以下三种情况:
2-(34)
3-(24)
4-(32)
依次递归,规模逐渐减小,直到只剩最后一个字符时,直接输出整个串就好了,这也就是递归的出口。
那么问题来了,
1.如何实现以2,或者3,4开头呢?
答案是,将他们所在的位置,和首位字符进行交换就可以了鸭;
即,2原来在s[1],我们只需交换s[0]与s[1]即可;同理,3开头的话,交换s[0]与s[2]。
2.如何才能保证不重复不遗漏呢?
答案是,保证递归前字符串的顺序不变,例如1 -(234)结束后,要将字符串回到1234的顺序,才开始做2-(134),同理,在2 -(234)结束后,要将字符串回到1234的顺序,才开始做3-(134);
为啥呢?
因为每次,换下一个字符开头时,我们要进行交换,如果交换了但不恢复原位的话,那么原来的位置就可能不是原来的字符了。
这样就会造成重复或者遗漏;
直接看代码:
#include <iostream>
using namespace std;
///将字符串s,从from到to,做全排列
void FullPermutation(char*s, int from, int to) {
if (from == to) {//递归出口,只剩最后一个字符了,按当前顺序输出字符
for (int i = 0; i <= from; i++) {
cout << s[i];
}
cout << endl;
return;
}
for (int i = from; i <= to; i++) {
swap(s[i], s[from]);//交换,即更改以另一个字符开头
FullPermutation(s, from + 1, to);//让下一位到结尾做全排列
swap(s[i], s[from]);//复位,很重要!!
}
}
int main() {
char s[500]; int m = 0; char temp;
cout << "please enter an array of char:\n";
cin >> s[m++]; //读取输入行的第一个字符(2)
while ((getchar()) != '\n') //读取输入行的第二个字符(“ ”)
{
cin >> temp;//读取输入行的第三个字符(3)
s[m++] = temp;
}
FullPermutation(s, 0, m - 1);
system("pause");
}
代码解析
当我第一次看到这个代码的时候,
for (int i = from; i <= to; i++) {
swap(s[i], s[from]);
看到这里我蒙了,咦?i=from,交换s[i], s[from],那不是没变吗?
没错,当第一次i=from时,确实没变,但是在循环里面,i会自增啊,例如,from=0;第一次,i=0,二者位置不变,这次结束后,i++,i=1,from仍然=0,此时交换就相当于换了开头的字符了。
关于递归结束后的复位操作,就是为了保证字符在原来的位置不变。
2.字符串中有重复字符呢?
例子
例如:1223
1-(223)
2-(123)
2-(213) 显然这种情况和上面一种重复
3-(122)
解决思路
我们只需要判断某个字符是否做过首字符就可以了呀,如果做过,那就不需要交换了,对不对。所以交换前,在源代码的基础上加一个判断就可以了
怎么标记字符是否做过首字符?
方法一:
例如当s[i]想要和s[0]交换,成为首字符时,我们循环判断一下,i前面的s[0…i-1]有没有和s[i]相同的,有则不能交换。但是这样又会对一层循环判断,增加时间复杂度。
///判断是否有相同的字符,如果有则不能移动
bool IsCanMove(char*s,int from ,int to) {
for (int i = from; i < to; i++) {
if (s[i] == s[to]) {
return false;
}
}
return true;
}
方法二:
空间换时间:
将所有字符是否做过首字符的标志位,存在一个数组中就行了,例如如果输入的全是单字符的话,就可以用mark[256]来记录,为什么是256呢?
因为ASCII表示的所有单字符字符都是8位,所以2^8=256,共256个字符,每个字符都可以 记录在此数组中。
代码
(主函数一致,就不再粘贴)
#include <iostream>
using namespace std;
///判断是否有相同的字符,如果有则不能移动
/*bool IsCanMove(char*s,int from ,int to) {
for (int i = from; i < to; i++) {
if (s[i] == s[to]) {
return false;
}
}
return true;
}*/
///将字符串s,从from到to,做全排列,不需要重复的
void FullPermutation(char*s, int from, int to) {
if (from == to) {
for (int i = 0; i <= from; i++) {
cout << s[i];
}
cout << endl;
return;
}
//如果是输入的全是单字符的话,可以采用mark[256]来标记每个字符
//ASCII码是8位。一般用前7位。是128个可用字符。2的7次方。
//IBM机器的扩展使用了最高位, 所以用2的8次方是256.
int mark[256];
for (int i = 0; i < 256; i++) {
mark[i] = 0;//初始全部置为零
}
for (int i = from; i <= to; i++) {
//if (!IsCanMove(s, from, i))
// continue;//结束本次循环
//在这里,如果使用上述判断方法,会多一次循环,时间复杂度会增加
if (mark[s[i]] == 1)
continue;
swap(s[i], s[from]);//交换
mark[s[i]] = 1;//如果该字符已经充当过首位,就将其标记,eg,字符a,mark['a']即,mark[97]==1
FullPermutation(s, from + 1, to);
swap(s[i], s[from]);//复位
}
}
3.非递归算法(可天然解决重复字符的情况)
思路:
整体思路
- 找字典序的下一个排列
对于任意一个字符串来说,总能找到该串的全排列中字典序下一个数,什么意思呢,就是全排列中比他大一点点的那个数。
那么这个全排列的起点就是:字典序最小的排列,eg.12345
终点就是:字典序最大的排列,eg.54321 - 将字符串排成升序,即从最小的开始依次求下一个排列
整个过程就是按照从小到大,依次找出下一个比他大一点点的数
举个例子:21543的下一个排列是23145,再找23145的下一个排列,依次下去,直到达到最大值
那么该如何计算下一个排序呢?
以21543为例:
- 逐位考察那个能增大
一个数右边有比它大的数存在,它就能增大
最后一个能增大的数是x=1,
- 1应该增大到多少?
增大到它右面比它大的最小的数,y=3 - 应该变成23XXX
- 显然后面的,XXX从小到大排序即可
- 得到23145
算法自然语言:
- 查找字符串中最后一位升序的位置i,
- 查找i后面的,即s[i+1…n-1]中比s[i]大的最小值s[j]
- 交换s[i],s[j](交换后s[i+1…n-1]一定是降序)
- 翻转i后面的字符,即s[i+1…n-1],(将其变为升序)
举个栗子:21543
从后往前查找最后升序的位置为1,然后找1后面比1大的最小的数字,3,交换二者,变成23541,然后将后面的串翻转,得到23145。
代码:
#include <iostream>
using namespace std;
//交换字符
void Swap(char *a, char*b) {
char t = *a;
*a = *b;
*b = t;
}
//从下标a到下标b进行翻转
void Reverse(char*a, char*b) {
while (a < b) {
Swap(a++, b--);
}
}
//求一只串的下一个字典序的全排列,此算法能天然的解决字符串重复的问题
bool Next_permutation(char a[]) {
char *pEnd = a + strlen(a);
if (a == pEnd) return false;
char *p, *q, *pFind;
pEnd--;//使pEnd值向最后一个字符
p = pEnd;//初始化p,从最后一位向左移动
while (p != a) {
q = p;
--p;
if (*p < *q)//从右往左相邻两位,如果升序
{
pFind = pEnd;//初始化pFind指针,从右往左寻找比*p大的数中最小的那个
while (*pFind <= *p) {
--pFind;//如果比*p小,指针左移
}
Swap(pFind, p);//找到后,交换二者位置;
Reverse(q, pEnd);//翻转后面的所有数;
return true;
}
}
Reverse(p, pEnd);//当找到最后一个最大的时,将其还原到原来的s
return false;
}
int main() {
//前面加一个排序算法使之升序即可,这里不赘述排序算法
char s[] = "1234";
//输出本身
for (int i = 0; i < strlen(s); i++) {
cout << s[i];
}
cout << endl;
while (Next_permutation(s)) {//当Next_permutation(s)不为false时循环调用
for (int i = 0; i < strlen(s); i++) {
cout << s[i];
}
cout << endl;
}
system("pause");
}
代码解释
只要理解算法,代码本身难度并不大,这里,只是指出一点自己当初遇到的困惑:
就是这里,明明当Next_permutation(s)不为false时才循环调用,那么为什么还在为false时加上Reverse(p, pEnd);呢?加不加对结果有影响吗?答案是:没影响!!对,确实不影响,但是,,,其实,若不将原来的翻转回去,即不复原的话,s字符串到最后会变成原串的翻转,假如原来的串是1234,在求完所有的全排列后,s串就变成了4321,假如要对此程序进行扩展,后面还有其他逻辑要用到s串呢?那不就出错了吗?因此,为了严谨,我们要把它翻转回去,所以在false时加上Reverse(p, pEnd);
解释:为什么该算法能天然解决重复字符?因为是按照字典序找的比它大的下一个,不可能找到等于自己本身的那个排列。