11习题2.8 输出全排列(编程题)【PTA浙大版《数据结构(第2版)》题目集】
1.原题链接
2.题目描述
请编写程序输出前 n n n个正整数的全排列( n < 10 n<10 n<10),并通过9个测试用例(即 n n n从1到9)观察 n n n逐步增大时程序的运行时间。
输入格式:
输入给出正整数 n n n( < 10 <10 <10)。
输出格式:
输出1到 n n n的全排列。每种排列占一行,数字间无空格。排列的输出顺序为字典序,即序列 a 1 , a 2 , ⋯ , a n { a_1, a_2, \cdots, a_n } a1,a2,⋯,an排在序列 b 1 , b 2 , ⋯ , b n { b_1, b_2, \cdots, b_n } b1,b2,⋯,bn之前,如果存在 k k k使得 a 1 = b 1 , ⋯ , a k = b k a_1=b_1, \cdots, a_k=b_k a1=b1,⋯,ak=bk 并且 a k + 1 < b k + 1 a_{k+1}<b_{k+1} ak+1<bk+1。
输入样例:
3
输出样例:
123
132
213
231
312
321
3.参考答案
答案一
#include<stdio.h>
int a[9];
void arrange(int m, int n){
int i,j,num;
if(m==n){
for(i=0;i<n;i++)
for(j=i+1;j<n;j++)
if(a[i]==a[j])
return;
for(i=0;i<n;i++)
printf("%d",a[i]);
printf("\n");
}
else{
for(num=1;num<=n;num++){
a[m]=num;
arrange(m+1, n);
}
}
}
int main(){
int n;
scanf("%d", &n);
arrange(0, n);
return 0;
}
答案二
#include<stdio.h>
int a[9];
void arrange(int m, int n){
int i,j,num;
if(m==n){
for(i=0;i<n;i++)
printf("%d",a[i]);
printf("\n");
}
else{
for(num=1;num<=n;num++){
for(j=0;j<m;j++)
if(a[j]==num)break;
if(j==m){
a[m]=num;
arrange(m+1, n);
}
}
}
}
int main(){
int n;
scanf("%d", &n);
arrange(0, n);
return 0;
}
答案三
#include <stdio.h>
int visited[10]={
0};
int list[10];
void dfs(int n,int m){
int i;
if(m==n+1){
for(int i=1;i<=n;i++)
printf("%d",list[i]);
printf("\n");
}
else{
for(i=1;i<=n;i++){
if(!visited[i]){
list[m]=i;
visited[i]=1;
dfs(n,m+1);
visited[i]=0;
}
}
}
}
int main(){
int n;
scanf("%d", &n);
dfs(n,1);
return 0;
}
答案四
#include <stdio.h>
int a[10];
void shift( int l, int i);
void shiftb( int l, int i );
void Per( int l, int r );
int main(){
int n, i;
scanf("%d", &n);
for (i=0; i<=n; i++) a[i] = i;
Per(1, n);
return 0;
}
void shift( int l, int i){
int j, t=a[i];
for (j=i; j>l; j--)
a[j]=a[j-1];
a[l] = t;
}
void shiftb( int l, int i ){
int j, t=a[l];
for (j=l; j<i; j++)
a[j] = a[j+1];
a[i] = t;
}
void Per( int l, int r ){
int i;
if (r==l) {
for (i=1; i<=r; i++) printf("%d", a[i]);
printf("\n");
}
else {
for (i=l; i<=r; i++) {
shift(l, i);
Per(l+1, r);
shiftb(l, i);
}
}
}
4.解题思路
本题输出前 n n n个正整数的全排列( n < 10 n<10 n<10),假如 n n n是固定的值,比如3,那么可以通过三重循环来实现全排列,实现方式如下:
#include<stdio.h>
int main(){
int i,j,k;
for(i=1;i<=3;i++){
for(j=1;j<=3;j++){
for(k=1;k<=3;k++){
if(i!=j&&j!=k&&i!=k)
printf("%d%d%d\n",i,j,k);
}
}
}
return 0;
}
但是本题中的 n n n是一个变量,如果用循环来实现,循环的层数是不确定,所以可以考虑用递归来实现。从循环的实现方式可以看出三点:
-
每一层循环按顺序选择一个数,调用递归自然也是每一层调用按顺序选择一个数;
-
循环到达最后一层时输出,调用递归函数自然也是递归调用到最后一层输出。
//m记录当前层级,若当前层级m到达最大层级n,输出排列完毕之后的数组 if(m==n){ for(i=0;i<n;i++) printf("%d",a[i]); printf("\n"); }
同时全排列的递归函数至少需要两个参数:1.当前调用的层级;2.最大的层级,即输入的 n n n。
//排列permutation void Per( int now, int n )
-
输出的每位数字不重复。如果要让输出的每位数字不重复有四种算法:
- 算法1:上面三重循环是在输出前检查每位数字的重复性,本题中的 n n n是不确定的,也就是数字的个数不确定,如果在输出前检查每位数字的重复性,需要用二重循环将每位数字与其他位的每位数字做比较,该算法最后一个测试点会超时。
- 算法2:优化算法1,不在最后检查数字的重复性,在进入每一层时检查。从前面示例的三重循环可以看出,每一层排列是遍历全部的数字,比如第一层循环是遍历1至3,第二层循环也是遍历1至3。检查当前层级选择的数字是否在前面层级出现过,若当前数字在前面的位置出现过,跳过该数字,若当前数字没有在前面的位置出现过,则将该数放入数组当前层级的位置。
- 算法3:进一步优化算法2,不使用遍历的方法检查重复性,给每一个使用的数字做标记,通过标记判断选择的数字是否可以使用。
- 算法4:先建立一个数组
int a[10];
,并初始化for (i=0; i<=n; i++) a[i] = i;
,调整数组元素的位置,对数组进行全排列,因为数组元素的值从1至9已经被确定下来了,不会出现不同位数字重复的情况。这是本题参考答案采用的做法。
5.答案详解
答案一详解
该算法最后一个测试点会超时。
枚举每个位置可能的取值,生成所有可能的排列,实现了生成1到n的整数的全排列,并使用数组来存储每个位置上的值,在输出前检查数字是否重复,将满足无重复元素的排列输出。
具体分析如下:
- 定义了一个全局数组
int a[9];
,用于存储每一次生成的排列。 - 定义了递归函数
void arrange(int m, int n);
,该函数的作用是生成排列。 arrange
函数的参数m
表示当前正在生成的排列的位置,n
表示需要生成的排列的长度。- 当m=n时,表示已经生成了一个完整的排列,此时需要检查该排列中是否有重复元素,如果有,则返回;否则,输出该排列,并换行。
- 当m<n时,需要继续生成排列,此时从1到n逐个尝试填入当前位置m,然后递归生成下一个位置的排列。
- 在
main
函数中,首先读入需要生成的排列长度n
,然后调用arrange
函数,将初始位置设为0
。
#include<stdio.h>
int a[9];
//m是当前层级,n是最大层级
void arrange(int m, int n){
int i,j,num;
//数组排列到n-1位置就排列完了,m==n排列完成输出
if(m==n){
//二重循环检查每位数字的重复性
for(i=0;i<n;i++)
for(j=i+1;j<n;j++)
if(a[i]==a[j])//如果有数字重复则不输出
return;
//如果程序执行到这里,则没有数字重复输出结果
for(i=0;i<n;i++)
printf("%d",a[i]);
printf("\n");
}
else{
//每一层都遍历所有的数字1至n
for(num=1;num<=n;num++){
a[m]=num;
arrange(m+1, n);//递归方程计算下一个层级的数字
}
}
}
int main(){
int n;
scanf("%d", &n);
arrange(0, n);
return 0;
}
答案二详解
在算法1的基础上优化,在每层选择数字时遍历前面的数字检查数字是否重复。
#include<stdio.h>
int a[9];
//m是当前层级,n是最大层级
void arrange(int m, int n){
int i,j,num;
//数组排列到n-1位置就排列完了,m==n排列完成输出
if(m==n){
for(i=0;i<n;i++)
printf("%d",a[i]);
printf("\n");
}
else{
//每一层都遍历所有的数字1至n
for(num=1;num<=n;num++){
//检查当前选择的数字num是否在前面的位置j出现过
for(j=0;j<m;j++)
//如果当前选择的数字num在前面的位置j出现过则退出此循环,遍历下一个数字
if(a[j]==num)break;
//结束上面的循环之后,若j==m,即当前选择的数字num在之前没有出现过
if(j==m){
a[m]=num;
arrange(m+1, n);//递归方程计算下一个层级的数字
}
}
}
}
int main(){
int n;
scanf("%d", &n);
arrange(0, n);
return 0;
}
答案三详解
深度优先搜索(DFS)。遍历所有组合,在每层选择数字时通过标记检查数字是否已被使用。
数组int visited[10]
记录一个数字是否被使用过,其中visited[i] = 0
表示数字i还没有被使用,visited[i] = 1
表示数字i已经被使用过。
数组int list[10];
表示生成的排列,其中list[i]
表示排列中第i
个位置上的数字。
函数dfs(n, step)表示搜索在生成排列的第step个位置时的情况,其中n表示排列的长度(即1~n),step表示当前搜索的位置。
当step = n+1时,已经生成了一个完整的排列,直接输出即可。
否则,依次尝试使用1~n中的数字作为当前位置的数字,并在visited数组中标记该数字已经被使用过,然后递归地搜索下一个位置。当回溯到当前位置时,需要撤销visited数组的标记。
#include <stdio.h>
int visited[10]={
0}; //做标记,数组下标对应相应的数字,没用过记作 0, 用过后记作 1
int list[10];
//m是当前层级,n是最大层级
void dfs(int n,int m){
int i;
//m==n+1时,n个层级排列完毕
if(m==n+1){
for(int i=1;i<=n;i++)
printf("%d",list[i]);
printf("\n");
}
else{
//每一层都遍历所有的数字1至n
for(i=1;i<=n;i++){
if(!visited[i]){
//如果选择的数字没用过
list[m]=i; //把它存入数组对应层级的位置
visited[i]=1; //标记为已使用
dfs(n,m+1); //进入下一层
visited[i]=0; //取消标记
}
}
}
}
int main(){
int n;
scanf("%d", &n);
dfs(n,1); //代表从第一层开始搜索
return 0;
}
答案四详解
调整数组元素的位置,对数组进行全排列。
具体分析如下:
- 定义了一个长度为
10
的整型数组a[]
,并声明了三个函数:shift()
、shiftb()
和Per()
。 - 在
main()
函数中,首先输入一个整数n
,然后用循环将数组a[]
初始化为从1到n的整数。 - 然后调用
Per()
函数。 - 在
Per()
函数中,如果左右两端点相同,则输出当前从1
到n
的全排列。 - 否则,对于左端点
l
到右端点r
的范围内的每一个i
,进行以下操作:- 调用
shift()
函数,将数组a[]
中第i
个元素移动到左端点l
的位置。 - 对
l+1
到r
的范围进行递归操作。 - 调用
shiftb()
函数,将数组a[]
还原到原始状态。
- 调用
#include <stdio.h>
int a[10];//程序中用不到a[0],只使用a[1]至a[9]
void shift( int l, int i);
void shiftb( int l, int i );
void Per( int l, int r );
int main(){
int n, i;
scanf("%d", &n);
for (i=0; i<=n; i++) a[i] = i;
Per(1, n);
return 0;
}
//对数组元素进行交换,将数组a[]中第i个元素移动到左端点l的位置
void shift( int l, int i){
int j, t=a[i];
for (j=i; j>l; j--)
a[j]=a[j-1];
a[l] = t;
}
//对数组元素进行交换,将数组a[]还原到原始状态
void shiftb( int l, int i ){
int j, t=a[l];
for (j=l; j<i; j++)
a[j] = a[j+1];
a[i] = t;
}
//排列permutation
void Per( int l, int r ){
int i;
//如果左右两端点相同,则排列完毕,输出当前的全排列
if (r==l) {
for (i=1; i<=r; i++) printf("%d", a[i]);
printf("\n");
}
else {
for (i=l; i<=r; i++) {
//将数组a[]中第i个元素移动到左端点l的位置
shift(l, i);
//每一层调用都会有一个for循环,for循环改变每一层函数调用的左边界位置的数字
Per(l+1, r);
//每一层会先改变了元素位置,内层函数执行完毕后,再把位置改变回去以便下一个循环排列
shiftb(l, i);
}
}
}
6.知识拓展
深度优先搜索
本题主要复习递归,关于深度优先搜索后面会做详细讲解,这里仅做简单介绍。
深度优先搜索(Depth-First Search,简称DFS)是一种常用的图遍历算法,它是通过深度遍历图中节点的方式来实现的。
具体来说,DFS算法从图的某个节点出发,首先访问该节点,然后选择一个相邻的未访问过的节点继续访问,直到所有相邻节点都被访问过。如果当前节点没有未访问的相邻节点,则回溯到上一个节点,继续访问它的未访问过的相邻节点,直到所有节点都被访问过。
DFS算法可以使用递归实现,也可以使用栈来实现。使用递归实现时,可以通过一个visited数组来记录已经访问过的节点,防止重复访问;使用栈实现时,则可以利用栈的后进先出(LIFO)的特性,来保存当前节点的未访问相邻节点。