目录
一、什么是递归
二、递归的思想
三、递归的限制条件
四、递归中的栈溢出
五、递归举例
(1)例1:n的阶乘
(2)例子2:顺序打印一个数的每一位
六、递归和迭代
七、拓展题目
(1)青蛙跳台阶问题
(2)汉诺塔问题
一、什么是递归
递归就是函数自己调用自己。下面举一个简单的递归例子(并不能解决问题,用法也不正确,只是让大家感受一下递归是什么样的):
在打印了很多次后终止了运行,但预期是一直死循环打印下去,为什么会这样?调试看看:
用F11进行调试,不进入第6行的main();,最后弹出了一个错误,如上图,Stack overflow(栈溢出),出现这个问题的原因在第四节讲。
二、递归的思想
将一个复杂的问题分解为相似的子问题,子问题再分解为更小的相似子问题,直到子问题不能再分解就结束分解。递归的递,表示递推,就是分解的过程;递归的归,表示回归,结束分解后再一个个返回子问题计算的结果,从小问题的解决到最后整个复杂问题的解决。
三、递归的限制条件
递归必须有以下两个条件:
- 递归必须有限制条件,这是递归中递推过程结束的条件。
- 每一次递归,要离限制条件更近。
四、递归中的栈溢出
在C语言中每次调用函数,都会向栈区申请一块内存空间,用于存放函数调用期间的各种信息(包括调用函数里局部变量的值等),这块空间被称为 运行时堆栈 或 函数栈帧。
每一次递归都会为调用的函数申请一块栈帧空间,只有函数返回后,才会释放对应的栈帧空间。如果递归太深而不返回,就会导致内存不够,发生栈溢出的问题。
这也是为什么递归必须要有限制条件的原因。
五、递归举例
(1)例1:n的阶乘
题目:计算n的阶乘(不考虑溢出,数太大了会发生溢出),n的阶乘就是1~n的数字累积相乘。
分析:
Fact(n)是求解n阶乘的递归函数,Fact(n - 1)是求解n - 1阶乘的递归函数,也是求解n阶乘的相似子问题。n等于0时,0的阶乘为1,是该递归函数的限制条件。
代码及运行结果:
递归过程图(红色是递推过程,蓝色是回归过程,绿色是返回的结果):
(2)例子2:顺序打印一个数的每一位
题目:输入一个整数m,按照顺序打印m的每一位数。
如:输入1234,打印1 2 3 4。
分析:对于数字n,使用n%10可以得到最后一位数。
9 > m ≥ 0时打印m,就是该递归的限制条件;Fact(m/10)就是相似子问题。这道题中的递归没有返回值,而是把返回值改为了打印数字。因为当m为个位数时,也可以用m%10表示,所有在代码中可以把两个条件下的打印部分统一在一起。
代码及运行结果:
六、递归和迭代
递归非常好用,可以把一个复杂的问题,以很简单的代码解决,但是它也有缺陷,在第四节讲的递归太深会造成很大的运行开销(无论是时间还是空间)。所以当一个问题用递归的方法很好解决,但是对于运行时的开销有很大的缺陷,递归的好用已经弥补不了这个缺陷时,就需要用迭代的方法(通常是循环的方式)解决了。
递归运行开销大这个缺陷,在斐波那契数列这道题中显著体现。
题目:求第n个斐波那契数。第一、二个数是1,后面的数都是它前两数的和。如:1,1,2,3,5......
① 递归的方法:
分析:
这道题可以用很简单的递归方法解决。
代码及运行结果:
当n较小时,比如10,很快计算出了结果。
当n较大时,比如50,计算特别慢,等了很久都没有结果出来。大家可以试试。
讨论一下运行慢的原因:
简略画一下计算第50个斐波那契数的递归过程图:
从上面的图可以看到有很多重复(同色)的计算,这个图只是画了一点,全部延伸下去直到遇到1和2才结束递推,可想而知要计算很多很多很多很多次。
用小点的n = 40做例子,看看计算了多少次Fact(3):
可以看到仅仅对于比较小的数40,它的Fact(3)就计算了这么多次!
所以这道题我们不能使用递归的方法,它的运行开销实在太大。改用循环的方法:
计算n=50,结果立马就运行出来了。注:把a、b、c换成long long类型是因为计算结果太大了,定义为int类型会溢出,看不到正确结果。仔细思考一下,这个计算时间只要50-2=48次循环而已。对比一下,用递归法算n=40,光计算Fact(3)都计算了39088169次。
七、拓展题目
(1)青蛙跳台阶问题
题目:一只青蛙一次可以跳1个或者2个台阶,请计算它跳n个台阶有几种跳法。
分析:
如果跳n个台阶,最后一跳要么是已经跳了n-1个台阶,最后一次跳一个台阶;要么是已经跳了n-2个台阶,最后一次跳两个台阶。Fact(n)表示跳n阶的跳法种数,可以拆分成 “最后跳一个台阶:跳n-1阶的跳法种数Fact(n-1)” 加上 “最后跳两个台阶:跳n-2阶的跳法种数Fact(n-2)”。对于限制条件(因为n=3的时候要算Fact(1)和Fact(2),所以限制条件为n=1和n=2两种情况):跳一个台阶时,只有一种跳法;跳两个台阶时,有两种跳法(跳两次一阶,或者跳一次两阶)。
代码及运行结果:
(2)汉诺塔问题
题目:如下图,有ABC三个杆,A杆上有n个盘子,请计算把A杆上的n个盘子全部移到C杆上,需要移动多少次盘子。
移动规则:
- 一次只能移一个盘子。
- 大盘子不能放在小盘子上。
- 可以以B杆作为中介柱子。
分析:
首先,若只有一个盘子,需要移动1次(A>C);若只有两个盘子,需要移动3次(小:A>B,大:A>C,小:B>C)。而n(n>1)个盘子,则可以类比移动两个盘子,看作上面有n-1个盘子和下面有一个盘子。因为最底下那个盘子最大,所以上面n-1个盘子中,随便哪个盘子都可以在它的上面。因此,上面的n-1个盘子移动时,可以把最底下的那个盘子忽略掉。【上面n-1整体:A>B,移动Fact(n-1)次;最大:A>C,移动1次;上面n-1整体:B>C,移动Fact(n-1)次】,加起来就是1+2*Fact(n-1)次。
为什么不把下面的n-1个作为整体,最小的看作另一个整体呢?如果把最小的看作一部分,下面的n-1个看作一部分,n-1个盘子移动时,就不能忽略掉最小的那个,【最小:A>B,移动1次;n-1整体:A>C,移动Fact(n-1)次;最小:B>C,移动一次】这个想法也就是错误的。因此,不能用Fact(n) = 2 + Fact(n-1)做递归。
代码及运行结果: