递归全排列
分析实现全排列的思路(两种思路)
第一种思路:
用函数功能的角度去分析递归、实现排列!
举一个例子:全排列 1,2,3
他的结果有6种情况:
1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2
-
分析:可以发现第一个数为不同数的时候都有两种情况,当然这里的两种情况是因为我们全排列的数为3所导致的,如果将数的范围增大你会发生其排列的情况也会随之增加。称之为
n-1
全排列种情况更为合适。 -
试想一下,如果第一个数保持不变,后面的数进行全排列,也就是后面的排列顺序都是不同的,那么加上第一个数他整体的排列情况依旧是互不相同的。
得出结论:第一个数不变后面的数进行全排列,其结果不变!
-
利用这个结论,将第一个数进行更换,后面的继续进行全排列,又是一些不一样的排列方式!第一个数的选择情况有[1-n]中可能,将所有的排列合起来就是n的全排列!
-
而后面的数要进行全排列的时候又可以使用相同的方式进行操作。操作完全一样只是操作的规模比前面的小1
-
依次类推,一直求到当只有一个数的时候他的全排列就只有一种情况。这时候我们要求的结果就出来了!
第一步:定义函数明确函数功能
那么我们就可以声明一个函数为perm()
这个函数是用来求全排列——>那要加一个参数,存放全排列数的数组arr[]
//对arr[]里面的数进行全排列
void perm(arr[]);
第二步:添加代码实现功能
依据我们刚才的分析,我们可以做一个循环将第一个数从1-n进行遍历,然后剩下的数进行全排列。
1.改变第一个数:将第一个数与后面的数逐个进行交换,就会实现遍历的功能
2.剩下的数进行全排列:使用同样的方式求剩下数的全排列,调用自身实现。
perm的功能是将数组里面的数进行全排列,而这时候要的功能是对指定范围内的数进行全排列:
因为perm只有一个参数,所以功能非常的单调,添加2个参数:begin、end表示范围的开始跟结束
begin,end:表示对数组arr[begin]~arr[end]范围里的数进行全排列
void perm(arr[],int begin,int end){
for(int i = 0; i < n; i++){
//改变第一个数
swap(arr,0,i);
//将剩下的数进行全排列
perm(arr,1,n);
}
}
加入参数begin和end之后就可以对全排列的范围进行控制。
如对n进行全排列:perm(arr,0,n);
递归就是不断调用自身,问题的规模逐渐缩小,所以其参数野要随着问题规模的改变而进行改变:
其参数都是对应调用时问题规模大小所确定的。所以在问题规模改变的时候参数也要进行修改!
而参数的改变往往都是有规律的。
void perm(arr[],int begin,int end){
//因为全排列的范围缩小,所以第一个数遍历的范围要缩小
for(int i = begin; i < n; i++){
//随着范围缩小第一个数也随着改变,begin表示全排列的第一个数,而不是原本的0
swap(arr,begin,i);
//begin表示全排列的第一个数,所以begin+1表示剩余的数进行全排列
perm(arr,begin+1,n);
}
}
第三部:添加终止条件
递归就是自己调用自己,如果没有设置终止条件则会无限的一直调用下去,直到电脑死机。
随着问题规模的变小,从n,n-1,n-2...3,2,1;
直到剩下一个数的时候可以轻易得知一个数的全排列是自身!就不用继续调用函数求全排列,可以终止递归!
添加下列代码:
if(begin == end) {
//这时候数组里面的结果就是我们想要的排列结果了,将其输出出来
for(int i = 0; i < n; i++){
printf("%d ",arr[0]);
}
//停止递归
return;
}
第四部:容易忽视的细节
第一个数要进行遍历,所以会跟后面的每一个数进行交换。
但是交换完之后要恢复原本的样子,不然第二次交换的数跟第一次交换的数不一样会导致排列的结果也不一样。
例如:
所以要在全排列完之后进行第二次交换之前将原本的数恢复原状
void perm(arr[],int begin,int end){
//因为全排列的范围缩小,所以第一个数遍历的范围要缩小
for(int i = begin; i < n; i++){
//随着范围缩小第一个数也随着改变,begin表示全排列的第一个数,而不是原本的0
swap(arr,begin,i);
//begin表示全排列的第一个数,所以begin+1表示剩余的数进行全排列
perm(arr,begin+1,n);
//恢复原状
swap(arr,begin,i);
}
}
完整代码:
#include<stdio.h>
int n;
void swap(int A[],int a,int b){
int tmp = A[a];
A[a] = A[b];
A[b] = tmp;
}
void dfs(int A[],int begin){
if(begin == n){
for(int j = 0; j < n; j++){
printf("%d ",A[j]);
}
printf("\n");
return;
}
for(int i = begin ;i < n; i++){
swap(A,begin,i);
dfs(A,begin+1);
swap(A,begin,i);
}
}
int main(){
scanf("%d",&n);
int A[n];
for(int i = 0; i< n; i++){
A[i] = i+1;
}
dfs(A,0);
return 0;
}
运行结果:
第二种思路:
用递归的运行结构去分析如果实现全排列
排列对于组合的区别就是,排列对于每个数的位置都是有区分的,如果位置不同就会导致整个排列的情况不一样。所以要对每一个位置的每一种情况都进行遍历判断!
举例子:
一个不透明的箱子里装着1,2,3三颗球,每一次取一颗,不放回。按顺序排列分别有多少种情况?
因为不放回,且按顺序排列,所以每一次取球都会使整个排列不同!所以要对每一次取球进行遍历判断
每一次取球都有剩下球的所有情况,所以要进行遍历,因为不放回所以每一次取球都会导致后面的结果发生改变从而导致这个排列不一致!
看这个结构与递归的结构就非常像,递归的结构也是倒着的树形结构,而且每层的规模都比上一层的规模小,所以可以运用递归求解。
定义递归函数(明确函数功能)
定义一个递归函数,其功能是每一次都在做,但是规模变少的操作——>选数。
第一次从三个里面选,第二次从两个里面选,第三次从一个里面选。而递归的层数表示第几次选。
用一个数组来存储每一个数的选取状态,实现被拿过的球不会再次被拿
void dfs(int select,int[] visit){
//select 表示选到了第几个数
//visit : 表示每个数的选取状态 1和0,被选对应的下标值为1
}
第二步:添加代码实现功能
为了要将结果输出出来,创建一个动态数组将所选的数保存起来:vector<int> save;
void dfs(int select,int[] visit){
//for循环表示对当前位置的选择可能进行遍历,select为几就是哪个位置进行遍历
for(int i = 0; i < n; i++){
//if进行判断,因为之前被选过的就不能再选了
if(visit[i] != 1){
//使用一个数组将所选的数保存起来
save.push_back(i+1);
//继续选下面的数,直到全部选完
dfs(select+1,visit[i]==1);
//将这个所选的数弹出去,不然会影响到第二次的遍历,在本层进行操作的只会对当前的数有效,不用担心会影响到递归后面的结果
visit[i] = 0;
save.pop_back();
}
}
}
第三步:容易忽视的细节
save.pop_back()
visit[i] = 0;
要在递归后面添加这一句恢复现场,如下图,递归的运行路径是一旦递归就将自己下面的所有子树全部运行完就是以这个数为根节点的所有情况都遍历完之后,才运行兄弟子树。
当第一个数存进去,进行递归之后就表示,这个位置存放当前这个数的所有排列情况已经完成了,所以当前存取这个数已经无用了,所以将其弹出去
而每一次递归后我们只弹出我们当次存进去的值,每一次层递归都会完成相同的操作,所以最后回溯回来的时候,每一层都会被弹出!
第四步:添加终止条件:
select表示第几次选择,而当select=n的时候表示已经是最后一次了,所以就应该终止选择了!
并且当拿到最后一次的时候所有的球也都拿起来了,所以这时候也可以将我们所拿的球输出出来。
if(select == n){
//输出结果
for(int i = 0; i < n; i++){
cout<<save[i]<<' ';
}
cout<<endl;
//终止
return 0;
}
完整代码:
#include<stdio.h>
#include<vector>
using namespace std;
int n;
vector<int> path;
int visit[10000] = {0};
void dfs(int begin,int state){
if(begin==n){
for(int i = 0; i < n; i++){
printf("%d",path[i]);
}
printf("\n");
return;
}
for( int i = 0 ; i < n; i++){
if(visit[i] != 1){
path.push_back(i+1);
dfs(begin+1,visit[i] = 1);
visit[i] = 0;
path.pop_back();
}
}
}
int main(){
scanf("%d",&n);
dfs(0,0);
return 0;
}
运行结果:
总结:
注意两种思路的运行结果是一样的,但是他的输出顺序是不同的,第二种思路的输出顺序是从小到大输出得,第一种思路并不是的!
if(visit[i] != 1){
path.push_back(i+1);
dfs(begin+1,visit[i] = 1);
visit[i] = 0;
path.pop_back();
}
}
}
int main(){
scanf("%d",&n);
dfs(0,0);
return 0;
}
#### 运行结果:
[外链图片转存中...(img-MgowSkc5-1600586869601)]
### 总结:
注意两种思路的运行结果是一样的,但是他的输出顺序是不同的,第二种思路的输出顺序是从小到大输出得,第一种思路并不是的!
总体感觉第一种思路会比较好理解,第二种思路要求对递归有一些了解,因为本身对递归不是很熟悉,所以第二题都想了好久,不过相同之后![584e0f1613cb2](https://img-blog.csdnimg.cn/img_convert/ad598873f8e0a1599f373a6d8f93ffad.png)对递归的运行结构就比较清晰了,有很明显的感觉!