前言
大家好,我是卷卷。首先恭喜大家坚持到现在,你们已经翻越了函数,数组,指针这“三座大山”,后面的内容就比较轻松了。本节课的主题是递归与宏定义,主要有以下几个部分:递归的概念,汉诺塔问题,长度单位转换,作业。(讨论q群号744931080,教程资源在群内)
一、递归的概念
把n的阶乘以递归方式进行定义:
求n!可以在(n-1)!的基础上再乘上n。如果把求n!写成函数fact(n),则fact(n)的实现依赖于fact(n-1)。这是fact函数的定义:
可以看到fact(n)调用了fact(n-1)。这种函数自己调用自己的方式称作递归调用。特别注意:必须要有递归出口,否则函数永远在调用,永远没有结果,计算机最终也会宕机。通过fact函数,我们能更进一步地了解递归的含义,即递归包括递推与回归两个过程。求n!,必须知道(n-1)!,求(n-1)!,必须知道(n-2)!,以此类推。一直递推到n=1为止,此时出现函数出口,所以立即向上层返回1,开始回归。通过一层层地往上传递,最终就得到了(n-1)!,返回n*(n-1)!即可。注意:一般递归出口放在函数体头部。
二、汉诺塔问题
例1:汉诺塔(Hanoi)问题源于一个古老的印度传说,后改成了一个游戏,游戏规则如下:有三根柱子A,B,C,A上放了n个大小不等的盘子,小盘在大盘上面。现要将A的盘子移到B上,必须满足如下条件:
(1)一次只能移动一个盘子
(2)小盘不能放在大盘下面
分析:首先如果只有1个盘子,直接移动即可。假设n大于1,则先将n-1个盘子从A移到C,再将第n个盘子从A移到B,最后将C上的n-1个盘子移到B。那么n-1个盘子如何移动呢?程序是给出解决问题的规律或方法,而不是一次次的具体展开。因此,不必关心具体n-1个盘子如何搬,只需将n个盘子简化为n-1个盘子的问题,n-1个盘子简化为n-2个盘子的问题……上述思想可设计出如下算法(伪代码):
hanoi(n个盘,A移到B){
if(n==1)
直接将A移到B;
else{
hanoi(n-1个盘,A移到C);
直接将n号盘从A移到B;
hanoi(n-1个盘,C移到B)
}
}
显然,递归出口是if的情况,递归式是else的情况。在C程序中,可用打印函数输出A移到B的信息来模拟盘子的移动。本题的重点是递归调用。代码:
#include<stdio.h>
void hanoi(int n,char src,char dest,char mid);
int main(){
int n;
printf("请输入盘子数:");
scanf("%d",&n);
printf("%c->%c的步骤如下:\n",'a','b');
hanoi(n,'a','b','c');
return 0;
}
void hanoi(int n,char src,char dest,char mid){
//src->dest,中间经过mid
if(n==1){
printf("%c->%c\n",src,dest);
return;
}
hanoi(n-1,src,mid,dest);//src->mid,中间经过dest
printf("%c->%c\n",src,dest);
hanoi(n-1,mid,dest,src);//mid->dest,中间经过src
}
hanoi函数中,src是源,dest是目的地,mid是中间过程。意思是从src传送到dest,中间要经过mid。如果只有一个盘子,直接输出src到dest即可。如果大于一个盘子,需要将A的n-1个盘子,先从src移到mid,然后将第n号盘子从src直接移到dest,最后将剩余的n-1个盘子从mid移到dest。主函数就不用多说了。我们来验证一下:
这就是本题的运行结果了。汉诺塔的结果有一个规律,n 个盘子有2^n - 1种步骤,相信大家已经对递归有所了解了。递归的好处是比较简洁直观,但是由于递归调用的过程需要暂时保存,以便于返回,所以会消耗比较多的内存空间。迭代无需消耗额外内存,但是没有递归直观。
例2:编写递归函数reverse(int n)实现将整数n逆序输出。分析:我们知道迭代版的算法是每次循环输出n%10,即n的最低位,然后通过n/=10砍掉最低位。根据题目,要先输出最低位,所以首先要输出最低位,即n%10。由于递归是最底层的先处理,所以当位数大于1时,更高位必须通过层层调用来输出,也就是以递归方式输出。即reverse(n/10);砍到只剩1位时可以直接输出,所以只剩1位的情况就是递归出口。代码:
void reverse(int n){
if(n<10){
printf("%d\n",n);
return;
}//递归出口
printf("%d",n%10);//最低位
reverse(n/10);//更高位
}
如果是个位数,直接输出即可,这段代码就作为递归出口。如果不是个位数,先输出最低位,然后再用递归调用的形式输出其余更高位。我在前面讲过,递归分为两个过程,递推与回归。递推的过程由于需要保留每一步的操作,从而需要消耗一定的内存空间,准确地来说是栈空间。栈是一种后进先出的数据结构,后进来的数据最先被弹出。所以这就和我们平时使用计算机一样,如果按ctrl+z,会回退到上一步,其实是弹出了栈顶元素。这就是递归调用的基本特性。
三、长度单位转换
例3:欧美国家长度使用英制单位,如英里、英尺、英寸等,其中1英里=1609米,1英尺=30.48厘米,1英寸=2.54厘米。请编写程序将输入的英里转换成米,英尺和英寸转换成厘米。分析:算法很简单,为了提高编程效率,使用宏定义来表示各长度。定义Mile_to_m位1609,Foot_to_cm为30.48,Inch_to_cm为2.54。本题的重点是宏定义,代码:
#include<stdio.h>
#define Mile_to_m 1609
#define Foot_to_cm 30.48
#define Inch_to_cm 2.54
int main(){
double foot,inch,mile;
printf("输入英里,英尺和英寸:");
scanf("%lf %lf %lf",&mile,&foot,&inch);
printf("%.2f英里=%.2f米\n",mile,mile*Mile_to_m);
printf("%.2f英尺=%.2f厘米\n",foot,foot*Foot_to_cm);
printf("%.2f英寸=%.2f厘米\n",inch,inch*Inch_to_cm);
return 0;
}
#define就是宏定义,作用是给某段文本起别名。比如第一个宏定义,意思是给1609起了一个别名:Mile_to_m,即用这串字符去替代1609,这样之后都可以使用这个别名了。其它两个宏定义以此类推。你或许会想到全局变量,但是全局变量与宏定义有本质的区别。全局变量首先是一个变量,它有数据类型要求,有指针等等,而宏定义只是单纯的文本替代,这句话是宏定义的核心,它只是单纯的文本替代。其实只要记住宏定义是单纯的文本替代,全局变量是变量,这样就不会搞混了。宏定义还可以定义函数,比如这样:
#define max(a,b) a>b?a:b
意思是传入参数a和b,如果a大于b就返回a,否则就返回b。我们可以来试一下:
这就是宏定义的概念与使用了,我们继续实验:
这就是本题的讲解了。
四、作业
作业是3道例题加5道练习题,总共8道题。练习1:
阶乘我想大家已经很熟悉了,因为之前已经做过很多有关阶乘的题,这题就不讲了。练习2:
本题也很简单,按照递归求阶乘的思想即可解决。即要知道n次幂,必须先知道n-1次幂,要知道n-1次幂,必先知道n-2次幂,以此类推。练习3:
这一题就更简单了,因为题目描述就是核心算法,但是要注意注意f(0)=0,f(1)=1,这两个都是递归出口。练习4:
这里需要重点讲一下十进制转二进制的算法,要用除二取余法。比如说十进制数6转二进制,转换过程如下:
6/2=3…0
3/2=1…1
1/2=0…1
上述过程就是每次除以2,得到整数商和余数,再把商除以2,以此类推,直到商为0。最后余数倒上来组合就是二进制,比如这里的110。
这就是除2取余法的基本介绍了,建议本题递归和迭代方式都实现一下,以更好地提升编程能力。最后一道题:
这一题和之前的例题也有一点类似,只要参照之前的例题即可。好了,这就是本讲的全部内容了,我们下讲见!