您的位置:首页 > 其它

字符全排列、全组合以及相关问题

2014-10-03 18:43 405 查看
最近,在看各种笔试题,看到了字符的全排列和全组合,及N皇后等相关问题,自觉它们的解题思路很类似,而且都可以使用递归的方法比较方便的解决,故做一总结。

PS:本文并非全部原创,解题思路和方法参看了一些比较优秀的博客,文章最后也列出来了参考的链接,但所列的代码都是自己编写的。

1. 字符的全排列

全排列在笔试面试中很热门,因为它难度适中,既可以考察递归实现,又能进一步考察非递归的实现,便于区分出考生的水平。

首先来看看题目是如何要求的:

用C++写一个函数, 如 Foo(const char *str),打印出 str 的全排列,

如 abc 的全排列:abc, acb, bca, dac, cab, cba

1) 全排列的递归实现

为方便起见,用123来示例下。123的全排列有123、132、213、231、312、321这六种。首先考虑213和321这二个数是如何得出的。显然这二个都是123中的1与后面两数交换得到的。然后可以将123的第二个数和每三个数交换得到132。同理可以根据213和321来得231和312。因此可以知道——全排列就是从第一个数字起每个数分别与它后面的数字交换。找到这个规律后,递归的代码就很容易写出来了:

第一种写法:

/**
* 全排列问题:不考虑有重复字符的问题
* 递归算法
* Author: zhongchun
* Time: 2014/09/21
*/

#include <stdio.h>
#include <string.h>

void Permutation(char *pStr, char *pBegin)
{
static int count = 0;
char *pCh = *pBegin;
char temp;

if(*pBegin == '\0')
{
count++;
printf("第%2d个排列是:%s\n",count,pStr);
}
else
{
for(pCh = pBegin; *pCh != '\0'; ++pCh)
{
temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;

Permutation(pStr, pBegin+1);

temp = *pCh;
*pCh = *pBegin;
*pBegin = temp;
}
}
}

int main()
{
char str[10] = {0};
int len = 0;

gets(str);

if(NULL == str)
return 0;

Permutation(str,str);

return 0;
}

运行结果:



第二种写法:

/**
* 全排列问题:不考虑有重复字符的问题
* 递归算法
* Author: zhongchun
* Time: 2014/09/21
*/

#include <stdio.h>
#include <string.h>

void Swap(char *a, char *b)
{
char tmp = *a;
*a = *b;
*b = tmp;
}

void Permutation(char *str, int k, int m)
{
int i;
static int count = 0;
if(NULL == str)
return;

if(k == m)
{
count++;
printf("第%2d个排列是:%s\n",count,str);
}
else
{
for(i = k; i <= m; ++i)
{
Swap(&str[i],&str[k]);
Permutation(str,k+1,m);
Swap(&str[i],&str[k]);
}
}
}

int main()
{
char str[10] = {0};
int len = 0;

gets(str);

if(NULL == str)
return 0;
len = strlen(str);

Permutation(str,0,len-1);

return 0;
}

运行结果:



2) 去掉重复的全排列的递归实现

由于全排列就是从第一个数字起每个数分别与它后面的数字交换。我们先尝试加个这样的判断——如果一个数与后面的数字相同那么这二个数就不交换了。如122,第一个数与后面交换得212、221。然后122中第二数就不用与第三个数交换了,但对212,它第二个数与第三个数是不相同的,交换之后得到221。与由122中第一个数与第三个数交换所得的221重复了。所以这个方法不行。

换种思维,对122,第一个数1与第二个数2交换得到212,然后考虑第一个数1与第三个数2交换,此时由于第三个数等于第二个数,所以第一个数不再与第三个数交换。再考虑212,它的第二个数与第三个数交换可以得到解决221。此时全排列生成完毕。

这样我们也得到了在全排列中去掉重复的规则——去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。用编程的话描述就是第i个数与第j个数交换时,要求[i,j)中没有与第j个数相等的数。下面给出完整代码:

/**
* 全排列问题:当有重复的字符时,去重
* 递归算法
* Author: zhongchun
* Time: 2014/09/21
*/

#include <stdio.h>
#include <string.h>

void Swap(char *a, char *b)
{
char tmp = *a;
*a = *b;
*b = tmp;
}

int IsSwap(char *pszStr, int nBegin, int nEnd)
{
int i;
for (i = nBegin; i < nEnd; i++)
if (pszStr[i] == pszStr[nEnd])
return 0;
return 1;
}

void Permutation(char str[], int k, int m)
{
int i;
static int count = 0;
if(NULL == str)
return;

if(k == m)
{
count++;
printf("第%2d个排列是:%s\n",count,str);
}
else
{
for(i = k; i <= m; ++i)
{
if(IsSwap(str, k, i))
{
Swap(&str[i],&str[k]);
Permutation(str,k+1,m);
Swap(&str[i],&str[k]);
}
}
}
}

int main()
{
char str[10] = {0};
int len = 0;

gets(str);

if(NULL == str)
return 0;
len = strlen(str);

Permutation(str,0,len-1);

return 0;
}

运行结果:



3) 全排列的非递归实现

要考虑全排列的非递归实现,先来考虑如何计算字符串的下一个排列。如"1234"的下一个排列就是"1243"。只要对字符串反复求出下一个排列,全排列的也就迎刃而解了。

如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"、"52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0、2都不行,5可以,将5和2交换得到"956220",然后再将替换点后的字符串"6220"颠倒即得到"950226"。

对于像"4321"这种已经是最“大”的排列,采用STL中的处理方法,将字符串整个颠倒得到最“小”的排列"1234"并返回false。

这样,只要一个循环再加上计算字符串下一个排列的函数就可以轻松的实现非递归的全排列算法。按上面思路并参考STL中的实现源码,不难写成一份质量较高的代码。值得注意的是在循环前要对字符串排序下,可以自己写快速排序的代码(请参阅《白话经典算法之六快速排序 快速搞定》),也可以直接使用VC库中的快速排序函数(请参阅《使用VC库函数中的快速排序函数》)。下面列出完整代码:

/**
* 全排列的非递归实现,去重
* Author: zhongchun
* Time: 2014/09/21
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//交换两个字符
void Swap(char *a, char *b)
{
char t = *a;
*a = *b;
*b = t;
}

//反转区间
void Reverse(char *a, char *b)
{
while (a < b)
Swap(a++, b--);
}

//下一个排列
int Next_permutation(char a[])
{
char *pEnd = a + strlen(a);
char *p, *q, *pFind;

if (a == pEnd)
return 0;

pEnd--;
p = pEnd;
while (p != a)
{
q = p;
--p;
//找降序的相邻数,前一个数即替换数
if (*p < *q)
{
//从后向前找比替换点大的第一个数
pFind = pEnd;
while (*pFind <= *p)
--pFind;
//替换
Swap(pFind, p);
//替换点后的数全部反转
Reverse(q, pEnd);
return 1;
}
}
Reverse(p, pEnd);//如果没有下一个排列,全部反转后返回true
return 0;
}

int QsortCmp(const void *pa, const void *pb)
{
return *(char*)pa - *(char*)pb;
}

int main()
{
int i = 1;
char szTextStr[100] = {0};
gets(szTextStr);

//加上排序,保证不同的输入序列的输出是一样的
qsort(szTextStr, strlen(szTextStr), sizeof(szTextStr[0]), QsortCmp);

do
{
printf("第%3d个排列\t%s\n", i++, szTextStr);
}while(Next_permutation(szTextStr));

return 0;
}

运行结果:







至此我们已经运用了递归与非递归的方法解决了全排列问题,总结一下就是:

1.全排列就是从第一个数字起每个数分别与它后面的数字交换。

2.去重的全排列就是从第一个数字起每个数分别与它后面非重复出现的数字交换。

3.全排列的非递归就是由后向前找替换数和替换点,然后由后向前找第一个比替换数大的数与替换数交换,最后颠倒替换点后的所有数据。

2. 字符的全组合

题目:输入一个字符串,输出该字符串中字符的所有组合。举个例子,如果输入abc,它的组合有a、b、c、ab、ac、bc、abc。

上面我们详细讨论了如何用递归的思路求字符串的排列。同样,本题也可以用递归的思路来求字符串的组合。

假设我们想在长度为n的字符串中求m个字符的组合。我们先从头扫描字符串的第一个字符。针对第一个字符,我们有两种选择:第一是把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选取m-1个字符;第二是不把这个字符放到组合中去,接下来我们需要在剩下的n-1个字符中选择m个字符。这两种选择都很容易用递归实现。下面是这种思路的参考代码:

/**
* 字符全组合的非递归实现,C++实现
* Author: zhongchun
* Time: 2014/09/28
*/
#include<iostream>
#include<vector>
using namespace std;
#include<assert.h>

void Combination(char *string ,int number,vector<char> &result);

void Combination(char *string)
{
assert(string != NULL);
vector<char> result;
int i , length = strlen(string);
for(i = 1 ; i <= length ; ++i)
Combination(string , i ,result);
}

void Combination(char *string ,int number , vector<char> &result)
{
assert(string != NULL);
if(number == 0)
{
vector<char>::iterator iter = result.begin();
for( ; iter != result.end() ; ++iter)
cout<<*iter;
cout<<endl;

return ;
}
if(*string == '\0')
return ;
result.push_back(*string);
Combination(string + 1 , number - 1 , result);
result.pop_back();
Combination(string + 1 , number , result);
}

int main()
{
//定义str不超过个字符
char str[10] = {0};

cin>>str;

if(NULL == str)
return 0;

Combination(str);

return 0;
}


注意:由于组合可以是1个字符的组合,2个字符的字符……一直到n个字符的组合,因此在函数void Combination(char* string),我们需要一个for循环。另外,我们用一个vector来存放选择放进组合里的字符。

运行结果:



3. 相关问题

1) N皇后问题

在N*N的棋盘上放置彼此不受攻击的N个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。N皇后问题等价于在N*N格的棋盘上放置N个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。如下图所示:8皇后问题的一个解。



*算法设计:

用N元组x[1:n]表示N皇后问题的解。其中,x[i]表示皇后i放在棋盘的第i行的第x[i]列。由于不允许将2个皇后放在同一列上,所以解向量中的x[i]互不相同。2个皇后不能放在同一斜线上是问题的隐约束。如果将N*N格的棋盘看做二维方阵,其行号从上到下,列号从左到右依次编号为1,2, …,n,从棋盘左上角到右下角的主对角线及其平行线(即斜率为-1的各个斜线)上,2个下标值的差(行号-列号)值相等。同理,斜率为+1的每一条斜线上,2个下标值的和(行号+列号)值相等。因此,如果2个皇后放置的位置分别是(i,
j)和(k, l),且i-j = k-l或i+j = k+l,则说明这2个皇后处于同一斜线上。以上两个方程分别等价于i-k = j-l和i-k = l-j。由此可知,只要|i-k| = |j-l|成立,就表明2个皇后位于同一条斜线上。

从上述分析中,可知可以使用回溯法解N皇后问题,用完全N叉树来表示解空间,使用可行性约束Place剪去不满足行、列和斜线约束的子树。

具体代码如下:

/**
* N皇后问题,C语言实现
* 问题:
* 在N*N的棋盘上放置彼此不受攻击的N个皇后
* 按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子
* N皇后问题等价于在N*N格的棋盘上放置N个皇后,任何个皇后不放在同一行或同一列或同一斜线上
*
* 算法设计:
* 用N元组x[1:n]表示N皇后问题的解。
* 其中,x[i]表示皇后i放在棋盘的第i行的第x[i]列
* Author: zhongchun
* Time: 2014/09/28
*/

#include <stdio.h>
#include <stdlib.h>

//统计总共有多少个解
int sum = 0;

//可行性约束条件
int Place(int k, int x[])
{
int j;
for(j = 1; j < k; ++j)
{
if((abs(k-j) == abs(x[k] - x[j])) || (x[k] == x[j]))
return 0;
}
return 1;
}

//递归函数
void Backtrack(int t, int x[], int n)
{
int i;

if(t > n)
{
sum++;
for(i = 1; i <=n; ++i)
printf("%d ",x[i]);
printf("\n");

}
else
{
for(i = 1; i <= n; ++i)
{
x[t] = i;
if(Place(t, x))
Backtrack(t+1, x, n);
}
}

}

int main()
{
int n;
int *x;
int i;

if(scanf("%d",&n))
{
x = (int *)malloc((n+1) * sizeof(int));

if(NULL != x)
{
//初始化n元组x的值都为
for(i = 0; i <= n; ++i)
x[i] = 0;

Backtrack(1,x,n);

printf("共有%2d种放法\n",sum);

free(x);

x = NULL;
}
else
{
printf("No More Space Left for the Array!\n");
return -1;
}

}

return 0;
}

运行结果:



2) 输入两个整数n和m,从数列1,2,3...n中随意取几个数,使其和等于m,要求列出所有的组合

思路:跟字符的全组合思路相似,将n分两种情况求解:①n加入,则n=n-1,m=m-n,递归;②n没有加入,则n=n-1, m=m,递归。具体代码如下:

/**
* C++实现
*
* Author: zhongchun
* Time: 2014/09/28
*/

#include <iostream>
#include <vector>
using namespace std;

void FindSum(int n,int sum,vector<int> &result)
{
//递归出口
if(n<=0||sum<=0)
return;

//输出找到的数
if(sum==n)
{
for(vector<int>::iterator iter=result.begin();iter!=result.end();iter++)
cout<<*iter<<"+";
cout<<n<<endl;
}

//n放在里面
result.push_back(n);
FindSum(n-1, sum-n, result);

//n不放在里面
result.pop_back();
FindSum(n-1, sum, result);
}

int main(void)
{
vector<int> result;
int sum,n;
cin>>n>>sum;
cout<<"所有可能的序列:"<<endl;
FindSum(n, sum, result);
return 0;
}

运行结果:





3) 打印从1到最大的n位数

这是个大数问题,需要使用字符串来处理。

第一种方法

用字符串表示数字的时候,最直观的方法就是字符串里每个字符都是’0’到’9’之间的某一个字符,用来表示数字中的一位。因为数字最大是n位的,因此,需要一个长度为n+1的字符串(字符串中最后一个是结束符号’\0’)。当实际数字不够n位的时候,在字符串的前半部分补0。

首先,把字符串中的每一个数字都初始化为’0’,然后每一次为字符串表示的数字加1,再打印出来。故只需做两件是:一是在字符串表示的数字上模拟加法,二是把字符串表示的数字打印出来。

具体代码如下:

/**
* 打印到最大的n位数
* 思路:在字符串上模拟数字加法
* C++实现
* Author: zhongchun
* Time: 2014/09/28
*/

#include <iostream>
#include <string>
using namespace std;

//打印数字,数字前半部分的不打印出来
void PrintNumber(char *number)
{
bool isBeginning0 = true;
int nLength = strlen(number);
for(int i = 0; i < nLength; ++i)
{
if(number[i] != '0' && isBeginning0)
isBeginning0 = false;

if(!isBeginning0)
cout<<number[i];
}
cout<<endl;

}

//在字符串表示的数字上模拟加法
bool Increment(char *number)
{
bool isOverflow = false;

int nLength = strlen(number);
int nCarryBit = 0;

int nSum = 0;

for(int i = nLength-1; i >=0; --i)
{
nSum = number[i] - '0' + nCarryBit;
if(i == (nLength-1))
nSum++;

if(nSum > 9)
{
if(i == 0)
{
isOverflow = 1;
}
else
{
nCarryBit = 1;
nSum = 0;
number[i] = nSum + '0';
}
}
else
{
number[i] = nSum + '0';
}
}

return isOverflow;
}

//
void PrintToMaxOfNDigits(int n)
{
if(n <= 0)
{
cout<<"输入值N不合法"<<endl;
return;
}

char *number = new char[n+1];
memset(number, '0', n);
number
= '\0';

while(!Increment(number))
{
PrintNumber(number);
}

delete []number;
}

int main()
{
int n;
cin>>n;

PrintToMaxOfNDigits(n);

return 0;
}

运行结果:



第二种方法

如果在数字前面补0的话,会发现n位所有十进制数其实就是n个从0到9的全排列。但跟上面所讲的全排列不一样,因为,这里的10个数字,即0到9,每一个数字都可以重复出现在任何一位上。

可以采用递归的方法实现,数字的每一位都可能是0-9中的一个数,然后再设置下一位。递归结束的条件是已经设置了数字的最后一位。

具体代码如下:

/**
* 打印到最大的n位数
* 思路:全排列问题,使用递归实现
*       数字的每一位都可能是-9中的一个数,然后再设置下一位
*       递归结束的条件是已经设置了数字的最后一位
* Author: zhongchun
* Time: 2014/09/28
*/

#include <iostream>
using namespace std;

//打印数字,数字前半部分的不打印出来
void PrintNumber(char *number)
{
bool isBeginning0 = true;
int nLength = strlen(number);
for(int i = 0; i < nLength; ++i)
{
if(number[i] != '0' && isBeginning0)
isBeginning0 = false;

if(!isBeginning0)
cout<<number[i];
}
cout<<endl;

}

//递归函数
void PrintToMaxOfNDigitsRecursively(char *number, int k, int m)
{
if(k == m)
{
PrintNumber(number);
}
else
{
for(int i = 0; i < 10; ++i)
{
number[k+1] = i + '0';
PrintToMaxOfNDigitsRecursively(number, k+1, m);
}
}
}

void PrintToMaxOfNDigits(int n)
{
if(n <= 0)
{
cout<<"输入值不合法"<<endl;
return ;
}

char *number = new char[n+1];
memset(number, '0', n);
number
= '\0';

for(int i = 0; i < 10; ++i)
{
number[0] = i + '0';
PrintToMaxOfNDigitsRecursively(number, 0, n-1);
}

delete []number;
}

int main()
{
int n;
cin>>n;

PrintToMaxOfNDigits(n);

return 0;
}

运行结果:



4) 射击运动员打靶问题

问题:一个射击运动员打靶,靶一共有10环,连开10枪打中90环的情况有多少种?需要考虑10枪的先后关系,比如9环10环同10环9环是不同的。
思路:跟N皇后问题的解题思路类似,进行回溯剪枝。
实现代码如下:
/**
* 一个射击运动员打靶,靶一共有10环,
* 连开10枪打中90环的可能行有多少种?
* Author: zhongchun
* Time: 2014/10/06
* sum用来记录共有只有多少种情况
* store[10]用来记录一种情况的10枪中的每一枪的得分
* score用来记录得分
*/
#include <iostream>
using namespace std;

int sum;
int store[10];

//输出其中的一种情况
void Output()
{
for(int i=9;i>=0;i--)
{
cout<<store[i]<<" ";
}
cout<<endl;
++sum;
}

void Comput(int num, int score)
{
//退出递归的条件
if(score<0 || score>(num+1)*10)
return ;

//打到最后一次
if(num==0)
{
store[num]=score;
Output();
}
else
{
//没有出现上述情况,则继续,即进行递归
for(int i=0;i<=10;i++)
{
store[num]=i;
Comput(num-1,score-i);
}
}
}

int main()
{
//10个元素的数组下标是0-9,所以打10枪的函数参数是9
Comput(9,90);
cout<<sum<<endl;
return 0;
}


参考:

1. http://blog.csdn.net/morewindows/article/details/7370155
2. http://blog.csdn.net/hackbuteer1/article/details/7462447
3. 剑指Offer名企面试官精讲典型编程题》

4. /article/2021280.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: