一、什么是递归?
在C语言中,递归就是函数自己调用自己。例如:
#include<stdio.h> int main() { printf("hehe\n"); main();//main函数中又调用了main函数 return 0; }
上面代码就是一个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死循环,导致栈溢出(Stack overflow)。
每一次函数调用都会占用一块内存空间: 函数栈帧空间(运行时堆栈)——在栈区上申请的空间
上面代码算是递归,但是是一个错误的释放,这里导致栈溢出。
所以函数是不能这样无限递归下去的,递归必须是有条件的!
1.递归的思想
递归的特点:使用少量的代码,就能完成非常复杂的任务。
递归的思考方式是把大事化小的过程(把大型复杂的问题拆分为小问题,拆分到不能再拆为止)。
递归中的递是递推的意思,归是回归的意思。
2.递归的限制条件
有2个必要条件:
①递归存在限制条件,当满足这个限制条件时,递归便不再继续。
②每次递归调用之后越来越接近这个限制条件。
二、递归举例
举例1.求n的阶乘(不考虑溢出),0的阶乘为0
Ⅰ.分析
n的阶乘公式:n!=n*(n-1)!
举例:5!=5*4*3*2*1
4!=4*3*2*1
所以:5!=5*4!
这样就把一个较大的问题转化为一个与原问题相似,但规模较小的问题来求解。
n的阶乘的递归公式如下:
Ⅱ.代码实现
#include<stdio.h> int Fact(int n) //或int Fact(unsigned int n) { if (n == 0) return 1; else return n * Fact(n - 1); } int main() { int n = 0; scanf("%d", &n); int ret = Fact(n); printf("%d\n", ret); return 0; }
运行结果不考虑n太大的情况,n太大存在溢出
分析:①这里递归的条件是:n>0,递归停下来的条件是:n=0
②不断接近跳出条件
Ⅲ.画图推演
举例2.顺序打印一个整数的每一位
输入一个整数n,打印这个按照顺序打印整数的每一位。
例如: 输入:1234 输出:1 2 3 4
Ⅰ.分析
如果n是一位数,n的每一位就是n自己,如果n超过一位数,就得拆分每一位。
1234%10就得到4,然后1234/10就等到123,就相当于去掉了4
然后继续对123%10就得到了3,再除10就去掉了3,以此类推,不断%10和/10,直到得到每一位
但是这样得到的数字顺序是倒着的。
我们假设写一个函数Print打印n的每一位:
Print(1234)——>Print(123) +4
Print(12) +3
Print(1) +2
Print(1234)可以拆分为两步(其它类似):
1.Print(1234/10) //打印123的每一位
2.Print(1234%10) //打印4
Ⅱ.代码实现
#include<stdio.h> void Print(int n) { if (n > 9) { Print(n / 10); printf("%d ", n % 10); } else { printf("%d ", n % 10); } } int main() { int n = 0; scanf("%d", &n); Print(n); //Print函数用来打印n的每一位 return 0; }
简化一下:
#include<stdio.h> void Print(int n) { if (n > 9) { Print(n / 10); } printf("%d ", n % 10); } int main() { int n = 0; scanf("%d", &n); Print(n); //Print函数用来打印n的每一位 return 0; }
分析:①递归的停止条件:n<=9
②n=n/10(每次都在接近停止条件)
Ⅲ.画图推演
三、递归与迭代
递归是一种很好的编程技巧,但也可能被误用,就像递归举例1一样,看到推到的公式,很容易写成递归的形式:
int Fact(int n) { if(n==0) return 1; else return n*Fact(n-1); }
Fact函数可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。
在C语言中每一次函数调用,都需要为本次函数调用在栈区申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
所以如果不想使用递归就要想其他的办法,通常是迭代的方式(通常是循环的方式)。
例如:计算n的阶乘,也可以 产生1~n数字累计乘在一起。
#include<stdio.h> int Fact(int n) { int i = 0; int ret = 1; for (i = 1; i <= n; i++) { ret *= i; } return ret; } int main() { int n = 0; scanf("%d", &n); int ret = Fact(n); printf("%d\n", ret); return 0; }
上面的代码能完成任务,并且效率比递归的方式更好。
有时候,写一个代码,虽然递归难以想到,但是使用递归写出的代码会非常简单,往往一个代码使用递归可能是几行代码,但是写成非递归(迭代)的方式,就是十几行甚至几十行代码。因此递归实现的简洁性便可以补偿它带来的运行时的开销。
如果递归的不恰当书写,会导致一些无法接受的后果,那我们就要放弃使用递归,使用迭代的方式解决问题!
举例3:求第n个斐波那契数
求第n个斐波那契数是不适合使用递归求解的,但是斐波那契数的问题是通过递归的形式描述的:
看到公式,我们很容易写成递归的形式:
#include<stdio.h> int Fib(int n) { if (n <= 2) return 1; else return Fib(n - 1) + Fib(n - 2); } int main() { int n = 0; scanf("%d", &n); int ret = Fib(n); printf("%d\n", ret); return 0; }
当我们输入50时,需要很长时间才能算出结果,这个计算所花费的时间是我们难以接受的,这也说明递归的写法是非常低效的,为什么呢?
其实递归程序会不断展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。
我们可以看到,在计算第40个斐波那契数的时候,使⽤递归⽅式,第3个斐波那契数就被重复计算了39088169次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。
我们知道斐波那契数的前2个都是1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了。
#include<stdio.h> int Fib(int n) { int a = 1; int b = 1; int c = 1; while (n >= 3) { c = a + b; a = b; b = c; n--; } return c; } int main() { int n = 0; scanf("%d", &n); int c = Fib(n); printf("%d\n", c); return 0; }
使用迭代的方式实现效率就会高很多。
有时候,递归虽好,但也会出现一些问题,所以我们不要迷恋递归,适可而止就好。