c语言从入门到实战——函数递归

函数递归

  • 前言
  • 1. 递归是什么?
  • 2. 递归的限制条件
  • 3. 递归举例
    • 3.1 举例1:求n的阶乘
      • 3.1.1 分析和代码实现
      • 3.1.2 画图推演
    • 3.2 举例2:
      • 3.2.1 分析和代码实现
      • 3.2.2 画图推演
  • 4. 递归与迭代


前言

函数递归是指一个函数直接或间接地调用自身,以解决问题的一种方法。在C语言中,函数递归可以用来计算阶乘、斐波那契数列等数学问题。


1. 递归是什么?

递归是学习C语言函数绕不开的一个话题,那什么是递归呢? 递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。

写一个史上最简单的C语言递归代码:

#include <stdio.h>
int main()
{
	printf("hehe\n");
	main(); //main函数中又调用了main函数
	return 0;
}

上述就是一个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死递归,导致栈溢出(Stack overflow)。

在这里插入图片描述

Stack overflow 就是栈溢出

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

2. 递归的限制条件

递归在书写的时候,有2个必要条件:

  • 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。 在下面的例子中,我们逐步体会这2个限制条件。

3. 递归举例

3.1 举例1:求n的阶乘

计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。

3.1.1 分析和代码实现

我们知道n的阶乘的公式:n! = n ∗ (n − 1)!

举例:
	5! = 5*4*3*2*1
	4! = 4*3*2*1
	所以:5! = 5*4!

这样的思路就是把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解的。

n!---> n*(n-1)!
 	 	
 	 		(n-1)! ---> (n-1)*(n-2)!

直到n是1或者0时,不再拆解

再稍微分析一下,当 n<=1 的时候,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 n*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\n", ret);
	return 0;
}

运行结果(这里不考虑n太大的情况,n太大存在溢出):

在这里插入图片描述

3.1.2 画图推演

在这里插入图片描述

3.2 举例2:

顺序打印一个整数的每一位 输入一个整数m,打印这个按照顺序打印整数的每一位。 比如:

输入:1234 输出:1 2 3 4

输入:520 输出:5 2 0

3.2.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)//就可以拆分为两步: 
Print(1234/10) //打印123的每⼀位
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;
}

输入和输出结果:

在这里插入图片描述
在这个解题的过程中,我们就是使用了大事化小的思路

把Print(1234) 打印1234每一位,拆解为首先Print(123)打印123的每一位,再打印得到的4把Print(123) 打印123每一位,拆解为首先Print(12)打印12的每一位,再打印得到的3 直到Print打印的是一位数,直接打印就行。

3.2.2 画图推演

以1234每一位的打印来推演一下
在这里插入图片描述

4. 递归与迭代

递归是⼀种很好的编程技巧,但是很多技巧⼀样,也是可能被误用的,就像举例1一样,看到推导的公式,很容易就被写成递归的形式:

在这里插入图片描述

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;
}

迭代的方式去实现这个代码,效率就要高出很多了。

有时候,递归虽好,但是也会引入一些问题,所以我们⼀定不要迷恋递归,适可而止就好。

拓展学习:

  • 青蛙跳台阶问题
  • 汉诺塔问题

以上2个问题都可以使用递归很好的解决,有兴趣可以研究。


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AirTag追踪汽车

美国华盛顿特区&#xff0c;11月4日&#xff0c;在一项全新的抗击车辆盗窃的措施中&#xff0c;市长穆里尔•鲍泽签署了一项新计划&#xff0c;将向该市车辆盗窃频率较高的社区居民免费提供苹果AirTag追踪器。 AirTag是苹果公司推出的一款蓝牙跟踪设备&#xff0c;它依靠Findm…

Android sqlite 使用简介

进行Android应用开发时经常会用到数据库。Android系统支持sqlite数据库&#xff0c;在app开发过程中很容易通过SQLiteOpenHelper使用数据库&#xff0c;SQLiteOpenHelper依赖于Context对象&#xff0c;但是基于uiatomator1.0和Java程序等无法获取Context的应用如何使用数据库呢…

JavaWeb课程复习资料——idea创建JDBC

1、创建空的Java Project 输入项目名称 空项目 2、引入jar包步骤 依次点击 File -> Project Structure&#xff08;快捷键 Ctrl Alt Shift s&#xff09;&#xff0c;点击Project Structure界面左侧的“Modules”如图&#xff1a; 在 【Dependencies】 标签界面下&…

Bean的四种实例化方式以及BeanFactory和FactoryBean的区别

2023.11.8 Spring为Bean提供了多种实例化方式&#xff0c;通常包括4种方式。 第一种&#xff1a;通过构造方法实例化第二种&#xff1a;通过简单工厂模式实例化第三种&#xff1a;通过factory-bean实例化第四种&#xff1a;通过FactoryBean接口实例化 通过构造方法实例化 创…

【Linux】vim

文章目录 一、vim是什么&#xff1f;二 、命令模式三、插入模式四、底行模式五、vim配置 一、vim是什么&#xff1f; Vim是一个强大的文本编辑器&#xff0c;它是Vi的增强版&#xff0c;支持多种语法高亮、插件扩展、多模式操作等功能。Vim有三种基本的工作模式&#xff1a;命…

2023年【电工(中级)】模拟考试题及电工(中级)复审模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【电工&#xff08;中级&#xff09;】模拟考试题及电工&#xff08;中级&#xff09;复审模拟考试&#xff0c;包含电工&#xff08;中级&#xff09;模拟考试题答案和解析及电工&#xff08;中级&#xff09;…

《强化学习与机器人控制》:探索深度学习的应用宝典

《强化学习与机器人控制》是一本涵盖了广泛主题的深度著作&#xff0c;它不仅介绍了人机交互控制和强化学习的基本原理&#xff0c;还深入探讨了无模型强化学习控制器以及其在机器人控制中的应用。这本书对于研究生和执业工程师来说是一本极具价值的参考书&#xff0c;它为读者…

go程序获取工作目录及可执行程序存放目录的方法-linux

简介 工作目录 通常就是指用户启动应用程序时&#xff0c;用户当时所在的文件夹的绝对路径。 如&#xff1a;root用户登录到linux系统后&#xff0c;一顿cd&#xff08;change directory&#xff09;后, 到了/tmp文件夹下。此时&#xff0c;用户要启动某个应用程序&#xff0…

基于Kinect 动捕XR直播解决方案 - 技术实现篇

一 安装与部署 1. 安装与部署Kinect-v2设备: 安装硬件: Kinect-v2设备带线一台; Kinect-v2 原装适配器适配器组合件设备一台; Kinect-v2 USB 3.0 WIndows PC 一天&#xff0c;原主板支持USB3.0接口; Windows PC 系统 Win10( Win 10 Version 21H2更新, 基于x64系统), 特别…

初阶JavaEE(14)表白墙程序

接上次博客&#xff1a;初阶JavaEE&#xff08;13&#xff09;&#xff08;安装、配置&#xff1a;Smart Tomcat&#xff1b;访问出错怎么办&#xff1f;Servlet初识、调试、运行&#xff1b;HttpServlet&#xff1a;HttpServlet&#xff1b;HttpServletResponse&#xff09;-C…

Redis 线程、持久化和监控

Redis 线程、持久化和监控 Redis线程模型 Redis主线程模型 图1 Redis 6.0之前的主线程模型 IO多路复用程序指的是单个线程监听多个套接字连接&#xff08;Socket&#xff09;&#xff0c;当IO多路复用程序将多个Socket上的就绪事件放置于队列中&#xff0c; Redis主线程一次处…

App备案-iOS云管理式证书 Distribution Managed 公钥及证书SHA-1指纹的获取方法

​ 根据近日工业和信息化部发布的《工业和信息化部关于开展移动互联网应用程序备案工作的通知》&#xff0c;相信不少要进行IOS平台App备案的朋友遇到了一个问题&#xff0c;就是apple不提供云管理式证书的下载&#xff0c;也就无法获取公钥及证书SHA-1指纹。 ​ 已经上架的应用…

1.UML面向对象类图和关系

文章目录 4种静态结构图类图类的表示类与类之间的关系依赖关系(Dependency)关联关系(Association)聚合(Aggregation)组合(Composition)实现(Realization)继承/泛化(Inheritance/Generalization)常用的UML工具reference欢迎访问个人网络日志🌹🌹知行空间🌹🌹 4种静态结构…

SHCTF-校外赛道

SHCTF-校外赛道 [WEEK1]babyRCE 1 (1)more:一页一页的显示档案内容2 (2)less:与 more 类似&#xff0c;但是比 more 更好的是&#xff0c;他可以[pg dn][pg up]翻页3 (3)head:查看头几行4 (4)tac:从最后一行开始显示&#xff0c;可以看出 tac 是 cat 的反向显示5 (5)tail:查看…

建链时,please install openssl! use “openssl version“ command to check.

please install openssl! use “openssl version” command to check. 但是我已经安装了 编辑build_chain.sh文件 也可以用vi或者gedit命令 将 [ ! -z “ ( o p e n s s l v e r s i o n ∣ g r e p 1.0.2 ) " ] ∣ ∣ [ ! − z " (openssl version | grep 1.0.2)…

Maven中的继承与聚合

一&#xff0c;继承 前面我们将项目拆分成各个小模块&#xff0c;但是每个小模块中有很多相同的依赖于是我们创建一个父工程将模块中相同的依赖定义在父工程中&#xff0c;然后子工程继承父工程Maven作用&#xff1a;简化依赖配置&#xff0c;统一依赖管理,可以实现多重继承像J…

Halcon WPF 开发学习笔记(0):开篇介绍

文章目录 文章专栏Halcon是什么&#xff1f;安装教学视频链接简单来说 Halcon快速开发环境确认新建项目 文章专栏 Halcon开发 Halcon是什么&#xff1f; 史上最全VisionPro和Halcon 的详细对比 Halcon简述 Halcon基础大全&#xff08;基础算子、高阶算子、数组、分割、字符检测…

基于C#的GRPC

GRPC gRPC&#xff08;gRPC Remote Procedure Call&#xff09;是由Google开发的高性能、跨语言的远程过程调用框架。它基于HTTP/2协议进行通信&#xff0c;支持多种编程语言&#xff0c;包括C, C#, Java, Python等&#xff0c;使不同语言的应用程序可以通过远程调用相互通信。…

Redis系列之常见数据类型应用场景

文章目录 String简单介绍常见命令应用场景 Hash简单介绍常见命令应用场景 List简单介绍常见命令应用场景 Set简单介绍常见命令应用场景 Sorted Set(Zset)简单介绍常见命令应用场景 Bitmap简单介绍常见命令应用场景 附录 Redis支持多种数据类型&#xff0c;比如String、hash、li…

Nat. Med. | 基于遗传学原发部位未知癌症的分类和治疗反应预测

今天为大家介绍的是来自Alexander Gusev团队的一篇论文。原发部位未知癌症&#xff08;Cancer of unknown primary&#xff0c;CUP&#xff09;是一种无法追溯到其原发部位的癌症&#xff0c;占所有癌症的3-5&#xff05;。CUP缺乏已建立的靶向治疗方法&#xff0c;导致普遍预后…