C语言第十四弹---函数递归

 

57abfd7f66fb47249f03bc2e7690ea5e.jpeg

  ✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】

函数递归

1、递归是什么?

1.1、递归的思想:

1.2、递归的限制条件

2、递归举例

2.1、举例1:求n的阶乘

2.1.1、分析和代码实现

2.1.2、画图推演

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

2.2.1、分析和代码实现

2.2.2、画图推演

3、递归与迭代

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

总结


 

1、递归是什么?

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

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! = 5*4*3*2*14! = 4*3*2*1所以:5! = 5*4!
这样的思路就是把⼀个较大的问题,转换为⼀个与原问题相似,但规模较小的问题来求解的。
当 n==0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。
n的阶乘的递归公式如下:
5f0057895b0441f6a89811e88f260978.png
那我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶
乘,函数如下:
int Fact(int n)
{if(n==0)return 1;elsereturn n*Fact(n-1);
}
测试:
#include <stdio.h>
int Fact(int n)
{if(n==0)return 1;elsereturn n*Fact(n-1);
}
int main()
{int n = 0;scanf("%d", &n);int ret = Fact(n);printf("%d\n", ret);return 0;
}
运行结果(这里不考虑n太⼤的情况,n太大存在溢出):
6673ed9bed754a788eb4eafd943c08af.png

2.1.2、画图推演

35f62bda36a14afb8b1fd58f44427529.png
建议初次学习递归时画上述的递归展开图,更容易理解是如何进行递归的。

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)

 

以此类推下去,就有
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;
}
输入和输出结果:
99b075f540b64e95aeb967bfb99a871e.png
在这个解题的过程中,我们就是使用了 大事化小的思路
把Print(1234) 打印1234每⼀位,拆解为首先Print(123)打印123的每⼀位,再打印得到的4
把Print(123) 打印123每⼀位,拆解为首先Print(12)打印12的每⼀位,再打印得到的3
直到Print打印的是⼀位数,直接打印就行。

2.2.2、画图推演

以1234每⼀位的打印来推演⼀下
f1819a4df7ca45dd939f15f5b2976b69.png

3、递归与迭代

递归是⼀种很好的编程技巧,但是很多技巧⼀样,也是可能被误用的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
bda704b53b9b45e681250aebd6269d8f.png
int Fact(int n)
{if(n==0)return 1;elsereturn 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.1、举例3:求第n个斐波那契数

我们也能举出更加极端的例⼦,就像计算第n个斐波那契数,是不适合使用递归求解的,但是斐波那契数的问题通过是使⽤递归的形式描述的,如下:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
f6c1e3c0aa484d43940150da2dac42ae.png
int Fib(int n)
{if(n<=2)return 1;elsereturn 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的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的, 这也说明递归的写法是非常低效的,那是为什么呢?
e857efe6578b493a97ac0a3cc435d196.png
其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计
算,而且递归层次越深,冗余计算就会越多。我们可以做个测试:
#include <stdio.h>
int count = 0;
int Fib(int n)
{if(n == 3)count++;//统计第3个斐波那契数被计算的次数if(n<=2)return 1;elsereturn 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;
}
输出结果:
6da1234c8ca9477eb40f1e73cddd31da.png
这里我们看到了,在计算第 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;
}
迭代的方式去实现这个代码,效率就要高出很多了。
有时候,递归虽好,但是也会引入⼀些问题,所以我们⼀定不要迷恋递归,适可而止就好。
拓展学习:
• 青蛙跳台阶问题
• 汉诺塔问题
以上2个问题都可以使用递归很好的解决,有兴趣uu可以研究喔。

总结


本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/658318.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Transformer 代码补充

本文是对Transformer - Attention is all you need 论文阅读-CSDN博客以及【李宏毅机器学习】Transformer 内容补充-CSDN博客的补充&#xff0c;是对相关代码的理解。 先说个题外话&#xff0c;在之前李宏毅老师的课程中提到multi-head attention是把得到的qkv分别乘上不同的矩…

【Java】Lombok的使用

一、Lombok是什么&#xff1f; Lombok是一个Java库&#xff0c;能自动插入编辑器并构建工具&#xff0c;简化Java开发。通过添加注解的方式&#xff0c;不需要为类编写getter或eques方法&#xff0c;同时可以自动化日志变量&#x1f680; 在我们封装一个类时&#xff0c;最常用…

CUDA编程- - GPU线程的理解 thread,block,grid - 学习记录

GPU线程的理解 thread,block,grid 一、从 cpu 多线程角度理解 gpu 多线程1、cpu 多线程并行加速2、gpu多线程并行加速2.1、cpu 线程与 gpu 线程的理解&#xff08;核函数&#xff09;2.1.1 、第一步&#xff1a;编写核函数2.1.2、第二步&#xff1a;调用核函数&#xff08;使用…

网络代理用途

网络代理的用途广泛&#xff0c;常用于代理爬虫&#xff0c;代理VPN &#xff0c;代理注入等。使用网络 代理能够将入侵痕迹进一步减少&#xff0c;能够突破自身IP的访问限制&#xff0c;提高访问速度&#xff0c; 以及隐藏真实IP &#xff0c;还能起到一定的防止攻击的作用。下…

ROS方向第二次汇报(2)

文章目录 1.本方向内学习内容&#xff1a;1.1.动作&#xff1a;1.1.1.案例接口定义:1.1.2.案例通信模型&#xff1a;1.1.3.服务器端代码&#xff1a;1.1.4.客户端源代码&#xff1a;1.1.5.动作命令行操作&#xff1a; 1.2.参数&#xff1a;1.2.1.查看参数列表&#xff1a;1.2.2…

dvwa靶场xss储存型

xss储存型 xxs储存型lowmessage框插入恶意代码name栏插入恶意代码 medium绕过方法 high xxs储存型 攻击者事先将恶意代码上传或储存到漏洞服务器中&#xff0c;只要受害者浏览包含此恶意代码的页面就会执行恶意代码。产生层面:后端漏洞特征:持久性的、前端执行、储存在后端数据…

4JS表达式和运算符expression and operator

表达式&#xff08;expression&#xff09;JavaScript中的一个短语&#xff0c;JavaScript解释器会将其计算&#xff08;evaluate&#xff09;出一个结果。程序中的常量是最简单的一类表达式。变量名也是一种简单的表达式&#xff0c;它的值就是赋值给变量的值。复杂表达式是由…

类与对象(中篇)

1、类的6个默认成员函数 如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。 2、构造函数---初始…

Java技术栈 —— Spring MVC 与 Spring Boot

参考文章或视频链接[1] Spring vs. Spring Boot vs. Spring MVC[2] Key Differences Between Spring vs Spring Boot vs Spring MVC

CRM系统主要干什么的

阅读本文&#xff0c;你将了解&#xff1a;一、CRM系统是什么&#xff1b;二、CRM系统主要干什么的&#xff1b;三、CRM系统在企业管理中的重要作用&#xff1b;四、企业落地案例分享——大吉包装。 本文所提及的功能演示和图片内容均来自于我们公司正在使用的简道云CRM系统&a…

【JVM】类加载流程

目录 1.加载 2.链接 &#xff08;1&#xff09;校验 &#xff08;2&#xff09;准备 &#xff08;3&#xff09;解析 3.初始化 4.使用 5.卸载 1.加载 加载阶段&#xff0c;简言之&#xff0c;查找并加载类的二进制数据&#xff0c;生成 Class 的实例 在加载类时&#x…

【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr

目录 1 -> 引用 1.1 -> 引用概念 1.2 -> 引用特性 1.3 -> 常引用 1.4 -> 使用场景 1.5 -> 传值、传引用效率比较 1.6 -> 值和引用作为返回值类型的性能比较 1.7 -> 引用和指针的区别 2 -> 内联函数 2.1 -> 概念 2.2 -> 特性 3 -…

Linux ---- Shell编程三剑客之AWK

一、awk处理文本工具 1、awk概述 awk 是一种处理文本文件的语言&#xff0c;是一个强大的文本分析工具。AWK是专门为文本处理设计的编程语言&#xff0c;也是行处理软件&#xff0c;通常用于扫描、过滤、统计汇总工作。用来处理列。数据可以来自标准输入也可以是管道或文件。…

共享的IP隔一段时间就变?用这种方法可以不需要知道电脑IP

前言 一般来说,电脑接入路由器之后,IP是由路由器自动分配的(DHCP),但如果隔一段时间不开机连接路由器,或者更换了别的网卡进行连接,自动分配的IP就会更改。 比如你手机连接着电脑的共享IP:192.168.1.10,但过段时间之后,电脑的IP突然变成了192.168.1.11,那么你的所有…

Qt+css绘制标题

之前学过html和小程序&#xff0c;帮老师做项目的时候也用过vue&#xff0c;在想qt绘制界面是不是也可以使用css,然后查了一些资料&#xff0c;绘制了一个标题&#xff0c;准备用到智能家居的上位机上面。 成果 源码 重写了paintEvent函数和TimeEvent函数&#xff0c;一个用于绘…

幻兽帕鲁服务器多少钱一个?26元,阿里云腾讯云华为云

2024年幻兽帕鲁服务器价格表更新&#xff0c;阿里云、腾讯云和华为云Palworld服务器报价大全&#xff0c;4核16G幻兽帕鲁专用服务器阿里云26元、腾讯云32元、华为云26元&#xff0c;阿腾云atengyun.com分享幻兽帕鲁服务器优惠价格表&#xff0c;多配置报价&#xff1a; 幻兽帕鲁…

透明拼接屏造型:多样拼接与影响因素

透明拼接屏&#xff0c;以其独特的透明显示效果和灵活的拼接方式&#xff0c;在现代显示领域中独树一帜。其造型多样&#xff0c;包括横屏拼接、竖屏拼接、异形拼接以及定制拼接等多种方式&#xff0c;满足了不同场景和应用的需求。尼伽小编将详细介绍这些拼接方式&#xff0c;…

PR转场模板|超级炫酷故障特效电影游戏视频转场PR模板剪辑素材

premiere转场&#xff0c;包含200个带有Sound FX的独特视频转场效果。加强剪辑视频视觉效果&#xff0c;在镜头之间的剪辑和添加文字动画&#xff01; MYFX Extension可帮助您一键浏览和应用预设&#xff01;可以喜爱预设&#xff0c;并拥有自己亲手挑选的库。如果您有任何问题…

字符数组的学习

前言&#xff1a; 在前面我们介绍过字符型数据是以字符的ASCII码储存在存储单元中&#xff0c;一般占一个字节&#xff0c;由于 ASCII码也属于整数类型&#xff0c;因此在C99标准中把字符类型归纳为整数类型中的一种&#xff0c;由于字符数据 的应用比较广泛&#xff0c;尤其…

抽象类(Java)、模板方法设计模式

一、概念 在Java中有abstract关键字&#xff0c;就是抽象的意思&#xff0c;可用来修饰类和成员方法。 用abstract来修饰类&#xff0c;那这个类就是抽象类&#xff1b;修饰方法&#xff0c;那这个方法就是抽象方法。 修饰符 abstract class 类名{修饰符 abstract 返回值类型…