第10讲:函数递归

news/2024/10/27 12:59:18/

目录

    • **1. 递归是什么?**
    • 2. 递归举例
    • 3. 递归与迭代

1. 什么是递归
2. 递归的限制条件
3. 递归的举例
4. 递归与迭代

正文开始

1. 递归是什么?

递归是学习C语言函数绕不开的⼀个话题,那什么是递归呢?
递归其实是⼀种解决问题的方法,在C语言中,递归就是函数自己调用自己。
写⼀个史上最简单的C语言递归代码:
#include <stdio.h>
int main()
{
printf(“hehe\n”);
main();//main函数中又调用了main函数
return 0;
}
上述就是⼀个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死递归,导致栈溢出(Stack overflow)。
在这里插入图片描述
1.1 递归的思想:

把⼀个大型复杂问题层层转化为⼀个与原问题相似,但规模较小的子问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把大事化小的过程。递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

1.2 递归的限制条件

递归在书写的时候,有2个必要条件:
• 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
• 每次递归调用之后越来越接近这个限制条件。

在下面的例子中,我们逐步体会这2个限制条件。

2. 递归举例

2.1 举例1:求n的阶乘
⼀个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。
⾃然数n的阶乘写作n!。
题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
2.1.1 分析和代码实现
我们知道n的阶乘的公式: n! = n ∗ (n − 1)!

举例:
5! = 54321
4! = 4321
所以:5! = 5
4!
这样的思路就是把⼀个较大的问题,转换为⼀个与原问题相似,但规模较⼩的问题来求解的。
当 n0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。
n的阶乘的递归公式如下:
在这里插入图片描述
那我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶
乘,函数如下:
int Fact(int n)
{
if(n
0)
return 1;
else
return nFact(n-1);
}
1
2
3
4
5
6
7
测试:
#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\n”, ret);
return 0;
}
在这里插入图片描述

2.2 举例2:顺序打印⼀个整数的每⼀位

输入⼀整数m,按照顺序打印整数的每⼀位。
比如:
输入:1234 输出:1 2 3 4
输入:520 输出:5 2 0

2.2.1 分析和代码实现

这个题目,放在我们面前,首先想到的是,怎么得到这个数的每⼀位呢?
如果n是⼀位数,n的每⼀位就是n自己
n是超过1位数的话,就得拆分每⼀位,1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4
然后继续对123%10,就得到了3,再除10去掉3,以此类推不断的 %10 和 /10 操作,直到1234的每⼀位都得到;
但是这里有个问题就是得到的数字顺序是倒着的
但是我们有了灵感,我们发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到
那我们假设想写⼀个函数Print来打印n的每⼀位,如下表示:

Print(n)
如果n是1234,那表示为:
Print(1234) //打印1234的每⼀位
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:

  1. Print(1234/10) //打印123的每⼀位
  2. printf(1234%10) //打印4
    完成上述2步,那就完成了1234每⼀位的打印
    那么Print(123)又可以拆分为Print(123/10) + printf(123%10)
    //打印3和12的每一位
    以此类推下去,就有
    Print(1234)
    ==>Print(123) + printf(4)
    ==>Print(12) + printf(3)
    ==>Print(1) + printf(2)
    ==>printf(1)
    直到被打印的数字变成⼀位数的时候,就不需要再拆分,递归结束。
    那么代码完成也就比较清楚:
    void Print(int n)
    {
    if(n>9)
    {
    Print(n/10);
    }
    printf(“%d “, n%10);
    }
    int main()
    {
    int m = 0;
    scanf(”%d”, &m);
    Print(m);
    return 0;
    }
    在这里插入图片描述
    在这个解题的过程中,我们就是使用了大事化小的思路
    把Print(1234) 打印1234每⼀位,拆解为首先Print(123)打印123的每⼀位,再打印得到的4
    把Print(123) 打印123每⼀位,拆解为首先Print(12)打印12的每⼀位,再打印得到的3
    直到Print打印的是⼀位数,直接打印就行。

3. 递归与迭代

递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误用的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
在这里插入图片描述
Fact(n)
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1);
}

Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及⼀些运行时的开销。在C语言中每⼀次函数调用,都需要为本次函数调用在内存的栈区,申请⼀块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧函数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调用中存在递归调用的话,每⼀次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采用函数递归的地方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
所以如果不想使用递归,就得想其他的办法,通常就是迭代的方式(通常就是循环的方式)。
如:比计算 n 的阶乘,也是可以产生1~n的数字累计乘在⼀起的。
int Fact(int n)
{
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成任务,并且效率是比递归的方式更好的。事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。当⼀个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

举例3:求第n个斐波那契数

我们也能举出更加极端的例⼦,就像计算第n个斐波那契数,是不适合使用递归求解的,但是斐波那契数的问题通过是使用递归的形式描述的,如下:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
在这里插入图片描述

int Fib(int n)
{
if(n<=2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
测试代码:
#include <stdio.h>
int main()
{
int n = 0;
scanf(“%d”, &n);
int ret = Fib(n);
printf(“%d\n”, ret);
return 0;
}
当我们n输⼊为50的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,
这也说明递归的写法是非常低效的,那是为什么呢
在这里插入图片描述
#include <stdio.h>
int count = 0;
int Fib(int n)
{
if(n == 3)
count++;//统计第3个斐波那契数被计算的次数
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);
printf(“\ncount = %d\n”, count);
return 0;
}
在这里插入图片描述
这里我们看到了,在计算第40个斐波那契数的时候,使用递归方式,第3个斐波那契数就被重复计算了
39088169次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。
我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了。
这样就有下面的代码:
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while(n>2)
{
c = a+b;
a = b;
b = c;
n–;
}
return c;
}
迭代的方式去实现这个代码,效率就要高出很多了。
有时候,递归虽好,但是也会引入⼀些问题,所以我们⼀定不要迷恋递归,适可而止就好。
完,感谢观看


http://www.ppmy.cn/news/1542344.html

相关文章

口腔疾病图文多模态知识图谱展示问答系统

毕业设计是不是总感觉找不到方向&#xff1f;别慌&#xff01;今天给大家推荐一个超级贴合现代技术创新的项目&#xff1a;口腔疾病图文多模态知识图谱展示问答系统&#xff0c;简直是为你量身定制的科技大礼包&#xff01; 项目亮点一&#xff1a;知识图谱展示 要研究口腔疾…

神经架构搜索:自动化设计神经网络的方法

在人工智能&#xff08;AI&#xff09;和深度学习&#xff08;Deep Learning&#xff09;快速发展的背景下&#xff0c;神经网络架构的设计已成为一个日益复杂而关键的任务。传统上&#xff0c;研究人员和工程师需要通过经验和反复试验来手动设计神经网络&#xff0c;耗费大量时…

中电信翼康工程师:我在 Apache SeaTunnel 社区的贡献之旅

贡献者Github ID&#xff1a;luckyLJY 文章整理&#xff1a;曾辉 Apache SeaTunnel 作为一款强大的数据同步和转换工具&#xff0c;凭借其部署易用性、容错机制、数据源支持、性能优势、功能丰富性以及活跃的社区支持&#xff0c;成为了数据工程师们不可或缺的利器。 因其具有的…

Linux 常用命令二

Linux 提供了许多命令来创建文件和文件夹。以下是一些常用的命令及其详细用法&#xff1a; 1. touch&#xff1a;创建空文件 touch 命令用于创建空文件&#xff0c;或者更新现有文件的访问和修改时间。 语法 touch [选项] 文件名 常用选项 -a&#xff1a;仅更新访问时间。-m&am…

HarmonyOS开发 - 本地持久化之实现LocalStorage支持多实例

用户首选项为应用提供Key-Value键值型的数据处理能力&#xff0c;支持应用持久化轻量级数据&#xff0c;并对其修改和查询。数据存储形式为键值对&#xff0c;键的类型为字符串型&#xff0c;值的存储数据类型包括数字型、字符型、布尔型以及这3种类型的数组类型。 在上一篇中&…

UnityShader——基础篇之学习Shader所需的数学基础——下

裁剪空间 顶点接下来要从观察空间转换到裁剪空间&#xff08;也被称为齐次裁剪空间&#xff09; 中&#xff0c;这个用于变换的矩阵叫做裁剪矩阵&#xff0c;也被称为投影矩阵 裁剪空间的目标是能够方便地对渲染图元进行裁剪&#xff1a;完全位于这块空间内部的图元将会被保留&…

CF-Loss:用于视网膜多分类血管分割和血管特征测量的临床相关特征优化损失函数|文献速递-基于生成模型的数据增强与疾病监测应用

Title 题目 CF-Loss: Clinically-relevant feature optimised loss function for retinal multi-class vessel segmentation and vascular feature measurement CF-Loss&#xff1a;用于视网膜多分类血管分割和血管特征测量的临床相关特征优化损失函数 01 文献速递介绍 视…

HarmonyOS 相对布局(RelativeContainer)

1. HarmonyOS 相对布局&#xff08;RelativeContainer&#xff09; 文档中心:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-layout-development-relative-layout-V5   RelativeContainer为采用相对布局的容器&#xff0c;支持容器内部的子元素设…