最近在学习递归,下面是近期遇见的一些习题,都可以使用递归法来解决的,总结如下,希望加深自己对递归的理解。
【题1】 字母全排列,假设给出字符串“abc”,求出它的所有排列,如abc,acb,bac,bca,等等。
扩展要求:不能出现重复的字符串,例如"abb",只能输出abb,bab,bba。(我还没做出来,^_^)
方法一,基于分治的递归法,见代码中arrange_div函数;方法二,基于回溯的递归方法,见代码中arrange_dfs函数。
#include <stdio.h> #include <string.h> #if 1 //将 1 改为 0 ,就可以隐藏下述代码 #define N 3 #define NUL '\0' int num=0; int locate[N+1]={0}; char charLocate[N+1]; void arrange_dfs(char *str); int main(){ char a[]="abc"; arrange_dfs(a); printf("%d\n",num); return 0; } void arrange_dfs(char *str){ int i,j; if(*str!=NUL){ for(i=1;i<=N;i++){ if(locate[i]!=0 ) continue; locate[i]=1; charLocate[i]=*str; arrange_dfs(str+1); locate[i]=0; } } else{ num++; for(j=1;j<=N;j++) printf("%c",charLocate[j]); printf("\n"); } } #endif #if 0 //将 0 改为 1 ,就可以运行下列代码 void arrange_div(char *str,int len); int num=0; void inv(char *a ,char *b){ char temp; temp=*a; *a=*b; *b=temp; } int main(){ char a[]="abc"; arrange_div(a,3); printf("%d\n",num); return 0; } void arrange_div(char *str,int len){ int i; if(0==len){ num++; printf("%s\n",str); }else { for(i=0;i<len;i++) { inv(&(*(str+i)),&(str[len-1])); arrange_div(str,len-1); inv(&(*(str+i)),&(str[len-1])); } } } #endif
此外,还想记录在写回溯法的递归函数时出现的一个非常隐蔽的错误。之所以出现这个错误,在于我对递归的理解不够深入,记在此处,时时提醒自己。
#include <stdio.h> #include <string.h> #define N 3 #define NUL '\0' int num=0; int locate[N+1]={0}; char charLocate[N+1]; void arrange_dfs(char *str); int main(){ char a[]="abc"; arrange_dfs(a); printf("%d\n",num); return 0; } void arrange_dfs(char *str){ int i,j; if(*str!=NUL){ for(i=1;i<=N;i++){ if(locate[i]!=0 ) continue; locate[i]=1; charLocate[i]=*str; arrange_dfs(str+1); } locate[i]=0; //错误在这里,没有回溯 } else{ num++; for(j=1;j<=N;j++) printf("%c",charLocate[j]); printf("\n"); } }
这道题目很简单,但又很典型。充分展现了两种截然不同的递归方法——回溯法和分治法。分治法以目标为驱动,在求解著名的斐波那契数列问题时,有一个公式淋漓尽致地诠释了分治法的思路,f(n)=f(n-1)+f(n-2)(n>2,n为正整数);分治法从最终的问题出发,倒推以前的状态,建立两者之间的关系。与此不同,回溯法从最初的状态,一步步试探,然后得到最终我们想求的结果。或许这又有点玄了。没事,继续来看习题。
【题目2】上\下楼梯问题,假设有h个楼梯,一次只允许走一步,两步,或者三步,求有多少种下楼梯的方法?
扩展要求:将每一种下楼梯的方案打印在屏幕。
方法一,基于分治的递归法,缺点是打印不了每一种下楼梯方案;方法二,基于回溯的递归方法,可以打印每种下楼方法。代码见下:
#include <stdio.h> #include <stdlib.h> #if 1 #define MAX 100 void trystep_hs(int n,int s); int pace[MAX]={0}; int num=0; int main(){ trystep_hs(5,1); printf("%d\n",num); return 0; } /*回溯法*/ void trystep_hs(int i,int s){ int j,k; for(j=1;j<=3;j++){ if(i<j) continue; else{ pace[s]=j; //如果将这一句放在后面的trystep_hs(i-j,s+1)前面,将会出错。 if(i==j){ num++; for(k=1;k<=s;k++) printf("%d ",pace[k]); printf("\n"); }else{ // pace[s]=j; 试一试 trystep_hs(i-j,s+1); } } pace[s]=0; } } #endif #if 0 int trystep(int n); int main(){ printf("%d\n",trystep(5)); return 0; } /*分治法*/ int trystep(int n){ if(n<=0){ printf("wrong input\n"); exit(1); } if(1==n) return 1; else if(2==n) return 2; else if(3==n) return 4; else return trystep(n-1)+trystep(n-2)+trystep(n-3); } #endif
或许,上述内容仍然没有清楚地阐述分治法与回溯法的具体思路。那只好祭出经典习题了,第一个习题是快速排序算法,它是分治法的经典代表;第二个是迷宫问题,它是回溯法的经典代表。看见这两道习题,就会令人自然而然地想起回溯法和分治法。
【2014/4/12补充】
其实无需刻意地写成分治或回溯的形式,使用一般的递归也可以求出此题,而且由此而写出的代码易于理解。
#include <stdio.h> void walk_floor(int now, int h); #define MAX 100 void trystep_hs(int n,int s); int pace[MAX]={0}; int cases_num; int num; int main() { int h; while(1){ printf("\n"); scanf("%d", &h); walk_floor(0, h); printf("%d\n", cases_num); trystep_hs(h, 1); printf("%d\n", num); } return 0; } void walk_floor(int now, int h) { int j; for( j = 1; j < 4; j++) { if( now + j >= h) { if( now+j == h) cases_num++; } else { walk_floor(now+j, h); } } } /*回溯法*/ void trystep_hs(int i,int s){ int j,k; for(j=1;j<=3;j++){ if(i<j) continue; else{ pace[s]=j; //如果将这一句放在后面的trystep_hs(i-j,s+1)前面,将会出错。 if(i==j){ num++; /* for(k=1;k<=s;k++) printf("%d ",pace[k]); printf("\n"); */ }else{ trystep_hs(i-j,s+1); } } pace[s]=0; } }
【题目3】假设有一个数组int a[]={1,2,3,4,7,2,3,8,9},将这个数组,由大到小,进行排序。(分治法)
c代码:
1 #include <stdio.h> 2 3 4 int part(int a[],int low,int high); 5 6 void quicksort(int a[],int low,int high); 7 8 9 int main(){ 10 int i; 11 int a[]={1,1,1,3,99,23,3,4,7,9,3,2,11,13,22,2,1}; 12 int len=sizeof(a)/sizeof(int); 13 quicksort(a,0,len-1); 14 for(i=0;i<len;i++) 15 printf("%d ",a[i]); 16 printf("\n"); 17 return 0; 18 } 19 20 /*函数part选定一个数X作为基准,对数组进行筛选,比X大的,放在X右边,反之,放在左边*/ 21 22 int part(int a[],int low,int high){ 23 int i,j,flag; 24 25 int partion=a[low]; //选定a[low]作为基准,这样便于处理 26 27 i=low;j=high; 28 /*循环查找比基准大的数和小的数,并移位。*/ 29 while(i<j){ 30 while(i<j&&a[j]>=partion) //此处一定要有等号"=" 31 j--; 32 a[i]=a[j]; 33 while(i<j&&a[i]<=partion) 34 i++; 35 a[j]=a[i]; 36 } 37 38 /*找到基准后,将其在数组上的位置返回,作为下一步递归使用*/ 39 a[i]=partion; 40 flag=i; 41 return flag; 42 43 } 44 45 46 /*进行分治法,递归处理*/ 47 void quicksort(int a[],int low,int high){ 48 49 if(low<high){ 50 int mid=part(a,low,high); //以基准为中心,开始分治处理 51 quicksort(a,low,mid); 52 quicksort(a,mid+1,high); 53 } 54 }
【题4】迷宫问题(回溯法)
扩展要求:除了递归实现以外,还可以用堆栈的形式非递归实现。此处不再赘述,感兴趣的话,可以参考:http://blog.csdn.net/lixiang99410/article/details/6179826
#include <stdio.h> #define MAX_SIZE 512 int maze[5][5]={ 0,1,0,0,0, 0,0,0,0,0, 0,0,0,0,0, 0,1,1,1,0, 0,0,0,1,0, }; struct point{int row,col;} trace[MAX_SIZE]; struct point status[2]={[0].row=1,[0].col=0,[1].row=0,[1].col=1}; void maze_tr(int row,int col,int s); int num=0; int main(){ maze[0][0]=2; maze_tr(0,0,1); } void maze_tr(int row,int col,int s){ int i,k; if(4==row&&4==col){ for(k=0;k<s;k++) printf("(%d,%d) ",trace[k].row,trace[k].col); printf("\n"); }else{ for(i=0;i<2;i++){ if((row+status[i].row)>4||(col+status[i].col)>4||(row+status[i].row)<0 ||(col+status[i].col)<0||2==maze[row+status[i].row][col+status[i].col]|| 1==maze[row+status[i].row][col+status[i].col]) continue; maze[row+status[i].row][col+status[i].col]=2; trace[s].row=row+status[i].row; trace[s].col=col+status[i].col; maze_tr(row+status[i].row,col+status[i].col,s+1); maze[row+status[i].row][col+status[i].col]=0; } } }
暂时告一段落,递归的方法千变万化,上面几种形式只是其中一小部分,而且递归算法效率未必高,存在优化问题,有空再来补充一些内容。递归还有许多应用,比如八皇后问题,归并排序算法,古老的打子弹计环数问题(见http://blog.csdn.net/hehe9737/article/details/7008552),每一种应用都灵活使用了递归思想,也都值得去做一做。
附件:清华大学湛卫军老师的c语言递归课件,非常不错。http://wenku.baidu.com/view/c744dd4333687e21af45a90a.html