函数递归-从认识到深入
函数递归
1.递归是什么
- 递归(Recursion)是编程中一种非常重要的概念,它指的是一个函数直接或间接地调用自身。递归在解决某些问题时非常有用,特别是那些可以分解为更小、更简单的子问题时。
1.1 “最简单的”递归程序
#include <stdio.h>
int main(){
printf("hello world\n");
main();//递归调用main函数(这是一个不推荐的做法,并且可能导致无限递归和栈溢出 )
return 0;
}
- 上述的代码是一个简单的递归程序,但是这里只是演示一下低随大的基本原理,代码可能存在死递归导致栈溢出(Stack overflow)的问题;
1.2 递归思想
- 把一个大型的复杂问题层层转化为一个与原问题相似的,但是规模较小的子问题来解决,直到子问题不能再被拆分成更小的子问题为止递归就结束了。所以递归思想就是把大事化小的逻辑过程;
- 递归这两个字可以分开来理解。递,递推。归,回归;
1.2.1 递归的限条件
- 递归存在限制条件,当满足这个限制条件时,递归便不再继续;
- 保证每次递归调用后结果越来越接近这个限制条件;
2. 递归的举例
2.1 求n
的阶乘
- 求阶乘的题目要求:计算n的阶乘(不考虑溢出),n的阶乘是1 ~ n的数字乘积相乘;
2.1.1分析和实现
- 什么时阶乘?阶乘(Factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。通常使用n!来表示n的阶乘。阶乘的计算公式为:
n! = n × (n - 1) × (n - 2) × … × 3 × 2 × 1 = n * (n - 1)!
例如:
5! = 5 × 4 × 3 × 2 × 1 = 120
4! = 4 × 3 × 2 × 1 = 24
3! = 3 × 2 × 1 = 6
2! = 2 × 1 = 2
1! = 1
0! = 1(按照数学定义,0的阶乘为1)
n的阶乘的递归公式如下:
所以我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶乘,函数如下:
int Fact(int n)
{
if(n==0)
{
return 1;
}
else
{
return Fact(n-1);
}
我们只需要在主函数中接收调用即可;
#include <stdio.h>
int Fact(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",ret);
return 0;
}
- 这里暂时不考虑n太大的情况,n太大会溢出,在后面的 迭代部分 会讲到此问题;
2.1.1.1递归-阶乘图解
2.2 顺序打印一个整数的每一位
- 题目要求:输入一个整数,按照原本的顺序打印整数的每一位
比如:
输入:1314 输出:1 3 1 4
输入:520 输出:5 2 0
2.2.1 分析和实现
1.如果输入的n
是一位数,则输出本身;如果输入的是大于1位的数字,那么就需要拆分;
2.如果输入的是1314
,那么1314%10
就能得到尾数4
,然后1314/10
就能得到131
,再用131%10
得到尾数1
,然后又将131/10
得到13
,以此类推,可以去掉尾数然后继续输出下一位,但是这里有一个问题就是输出的数字顺序是倒序输出的;
- 我们发现数字的最低位是最容易得到的,直接通过
n%10
即可,所以我们可以写一个Print
函数打印n
的每一位 - 我们打印每一位的步骤是:
1.Print(1234/10
)//打印123的每一位
2.printf(1234%10)
//打印4 Print(1234)
Print(123)
=====>printf(4)
Print(12
)====>printf(3)
Print(1)
==>printf(2)
printf(1)
直到被打印的数字变成一位数字的时候就无需继续拆分,到此递归结束;
#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);
return 0;
}
总结:
- 把
Print(1314)
打印每一位,拆解为先用Print(131)
打印131
的每一位,再打印4
; - 把
Print(131)
打印每一位,拆解为先用Print(13)
打印13
的每一位,然后打印1
; - 以此类推,直到打印的是一个数字;
2.2.1.1 递归-推演图解
3.递归与迭代
迭代:通过循环结构(如for、while
等)来重复执行一段代码,直到满足某个条件时停止。迭代不涉及到函数调用自身,而是通过更新循环变量来控制循环的次数。
迭代的优点在于它通常比递归更高效,因为它避免了递归调用带来的额外开销。此外,迭代在处理大量数据时通常更加安全,因为它不太可能导致栈溢出。然而,对于某些问题,递归可能更直观,更容易实现。
-
刚刚阶乘的例子我们根据公式的推导,很容易想到使用递归要解决,就像刚刚的代码:
但是,Fact
函数在对于一些较小的输入值时求阶乘,可以准确的求出结果,但是对于求一些较大的值的阶乘可能无法求出,因为递归函数调用的过程设涉及一些运行时的开销问题; -
在C语言中,每次函数调用都需要为本次函数调用在栈区申请一块内存空间,用来保存函数调用期间的各种局部变量的值。这块空间被称为运行时堆栈,或者函数栈帧。
-
如果函数不返回,函数对应的栈帧空间就会一直占用。因此,如果函数调用中存在递归调用,每一次递归函数调用都会开辟属于自己的栈帧空间。直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
-
因此,如果采用函数递归的方式完成代码,递归层次太深就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
所以我们刚刚计算阶乘的题目可以使用 迭代的方式 来进行:
#include <stdio.h>
int main() {
int n = 0;
scanf("%d", &n);
int ret = 1;
for (int j = 1; j <= n; j++)
{
ret *= j;
}
printf("%d",ret);
return 0;
}
- 用迭代的方式效率比递归更好;
- 许多问题我们都以递归的形式来解释,这是因为它比递归的形式更加清晰,但是再这些问题上,迭代的实现往往会比递归的实现效率更高;
- 所以一般来说,如果问题本身具有递归结构(如树形结构或分治算法),并且递归实现简单明了,那么递归可能是一个好的选择。然而,如果问题更适合通过循环结构来解决,或者递归实现可能导致性能问题,那么应该选择迭代。
- 当复杂的问题难以用迭代是实现时,此时递归的简洁性便可以弥补它所带来的运行时的开销;
3.1 求斐波那契数
- 我们说计算斐波那契数数不是适合用递归求解的,但是斐波那契数的问题可以通过递归的形式描述,让我们更好的理解:
看到这个示例图我们很容易想到递归:
#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;
}
上面的代码,又是确实可以计算出正确结果,但是如果我们输入的值太大,就需要很长时间才能计算出结果,这说明递归的写法效率太低:
在这个递归程序中,它会不断展开,在展开过程中我们会发现有很多重复计算的过程,而且递归层次越多,重复计算的地方就越多。
我们可以做一个次数计算:
#include <stdio.h>
int count = 0;
int Fib(int n)
{
if (n == 4) {
count++;
}
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", count);
return 0;
}
我们可以看到在计算第30
个斐波那契数的时候,第4
个斐波那契数就被重复计算了196418
,所以用递归来计算斐波那契数是非常不划算不明智的,故我们可以用迭代来实现
我么知道斐波那契数的前两个都是1
,然后前2
个数相加就是第3
个数,以此类推:
#include <stdio.h>
// 计算斐波那契数列的第n项的函数
int Fib(int n)
{
// 初始化变量a和b为斐波那契数列的前两项
int a = 1;
int b = 1;
// 初始化变量c为a和b的和,用于存储下一个斐波那契数
int c = 1;
// 当n大于2时执行循环,用于计算第n项斐波那契数
while (n > 2)
{
// 更新a和b的值
a = b;
b = c;
// 计算下一个斐波那契数,并存储在c中
c = a + b;
// 递减n的值,以便在下次循环中检查是否继续
n--;
}
// 返回计算得到的斐波那契数
return c;
}
int main()
{
// 初始化变量n用于存储用户输入的数
int n = 0;
// 从标准输入读取用户输入的数
scanf("%d", &n);
// 调用Fib函数计算斐波那契数,并将结果存储在ret中
int ret = Fib(n);
// 打印计算得到的斐波那契数
printf("%d\n", ret);
// 程序正常结束,返回0
return 0;
}
通过迭代的方式去实现效率会高很多