一、问题:
给出包含n个数字(可能重复)的数组P,打印出其全排列
二、思路:
首先想的是能不能用数学的方法来解决这个问题,很遗憾的我们只记得可以算出全排列的个数,要把排列结果全部输出是不可能的。
那么再考虑一下用暴力求解(brute force)的方法,也就是最naive,最直接的办法:从P中每次取出一个与已经取出的数字不重复的数字(这里需要保证不重复)
,经过n次(这里已经保证了不会遗漏),就可以得到一个排列结果(这里需要保证排列结果不重复不遗漏)。这种思路很好理解,但是存在着很明显需要解决的问题:
1. 每次生成排列时,如何实现不遗漏不重复的“取”数字?
只要取n次,每次取得数字不重复,那么自然就不会遗漏,因此关键在于不重复:可以考虑把取出的数字放在一个数组A中,下次取的时候通过和此数组对比来决定取哪个数字。这里面还有个细节,如果P中的n个数是互不重复的,那么只需要取与A中数字不等的数字即可,但是如果P中的n个数字有重复的话,则可以取与A中数字
相等的数,只要这个相等的数字在A中的出现次数小于原数组P中的总次数即可。
2. 如何不遗漏不重复地取得所有的排列结果?
如果天马行空地取数,很有可能不断地取到重复的排列,最后导致永远都无法不遗漏取出所有的不重复的排列。为了达到不遗漏不重复的目的,对于不同排列的构造顺序是有要求的。这个构造顺序的设计就是本算法的核心所在。我们耐心的来分析,简单起见,先考虑n个数字不重复的情况,再简化一下,假设就是1,2,3,4这四个数字(特例,简化是思维中最常见,最自然但是也最有效的方法)。我们就来列出它的全排列,首先自然而然我们把排列分为了4组,1开头的,2开头的,3开头的和4开头的,下面分别把他们列出来:
先列1开头的:
1 2 3 4
1 2 4 3
1 3 2 4
1 3 4 2
1 4 2 3
1 4 3 2
再列2开头的:
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
2 4 1 3
2 4 3 1
3开头的和4开头的不用列了,因为在自动动手列的过程中,我们已经感受到了其中的规律。对于每个排列,我们脑海中做的事情是这样的,确定第1个数字(分4组就是在做这个事情),确定剩下的数字(以1开头的排列为例,我们很自然的把剩下的2,3,4这三个数字做了全排列)而对于2,3,4这三个数字的全排列,我们的做法仍然是:确定第一个数字,然后确定剩下的数字。这里的逻辑最合适的实现的方法就是递归。因为适合用递归来解决的问题的特点是:可以分步骤来解决的问题,而且每一步里面都有一个不变的逻辑。而本问题可以完美地匹配到这些特点。对于更复杂的情况,也满足这个结论。
三、代码
1. 伪代码
cur:当前步数
A:当前已经存放了cur个数字的数组
不变逻辑:从1~n中选出不属于A的数组,放入A,cur++,进入下一步。
递归边界:cur==n
2.排序函数和测试主函数
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <algorithm>
using namespace std;
int int_compare(const void* _a,const void* _b);
void Permute1(int n,int* A);// 使用STL里面的库函数
void Permute2(int n, int* P, int* A, int cur);// 从n由小到大的横向分割排列的角度
#define MAX_PERMUTATION_LEN 20
int Permutation()
{
FILE* fp;
int n;
int A[MAX_PERMUTATION_LEN],P[MAX_PERMUTATION_LEN];
fp = fopen("Permutation.txt","rt");
if (NULL == fp)
{
printf("Can not find input file!");
return -4;
}
while(1){
if (1 != fscanf(fp,"%d",&n))
{
printf("Can not read in length!");
fclose(fp);
return -1;
}
if (n>MAX_PERMUTATION_LEN)
{
printf("Too long!");
fclose(fp);
return -2;
}
if (n == 0) // end flag
{
return 0;
}
for (int i=0; i<n; i++)
{
if (1 != fscanf(fp,"%d",&P[i]))
{
printf("Can not read in number!");
fclose(fp);
return -3;
}
}
qsort(P,n,sizeof(int),int_compare);
//Permute1(n,P);
Permute2(n,P,A,0);
printf("-------------------\n");
}
//Permute2(n,A,0);
fclose(fp);
return 0;
}
void Permute1(int n,int* A)
{
do
{
for (int i=0; i<n; i++)
{
printf("%d ",A[i]);
}
printf("\n");
} while (std::next_permutation(A,A+n));
}
void Permute2(int n, int* P, int* A, int cur)
{
int i,j;
if(n == cur){
for (i=0; i<n; i++)
{
printf("%d ",A[i]);
}
printf("\n");
}else {
for (i=0; i<n;i++)
if (i==0 || P[i] !=P[i-1])
{
int c1=0,c2=0;
for (j=0; j<cur; j++) if (A[j] == P[i]) c1++;
for (j=0; j<n; j++) if (P[j] == P[i]) c2++;
if (c1<c2)
{
A[cur] = P[i];
Permute2(n,P,A,cur+1);
}
}
}
}
int main()
{
Permutation();
return 0;
}
3.测试文件
3
1 1 1
4
1 1 2 2
5
1 2 3 4 5
0
四、总结
1. 一个问题,不是由人,而是交给计算机解决,这就是算法要做的事情。由于计算机的特殊性,必须把人头脑里面的方法用代码的方式告诉计算机。这个代码的最大特点之一就是:必须是具可重复的模式。
2. 解答树:如果一个问题的解可以由多个步骤获得,而每个步骤都有若干选择(这些选择可能依赖于先前的选择),且可以递归枚举实现,这可以用解答树来描述。