字符串的全排列递归&非递归算法

字符串的全排列

写在前面:本文章为根据七月算法的讲解后的学习笔记与心得。

问题描述

给定一个字符串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);

解释:为什么该算法能天然解决重复字符?因为是按照字典序找的比它大的下一个,不可能找到等于自己本身的那个排列。

发布了2 篇原创文章 · 获赞 2 · 访问量 64

猜你喜欢

转载自blog.csdn.net/K__Ming/article/details/105278658