从结构嵌套的幻梦里:递归与数据构建的精巧和鸣

news2025/1/22 13:34:10

在这里插入图片描述

大家好啊,我是小象٩(๑òωó๑)۶
我的博客:Xiao Xiangζั͡ޓއއ

很高兴见到大家,希望能够和大家一起交流学习,共同进步
在这里插入图片描述
这一节我们来学习递归的相关知识

函数递归

  • 一、什么是递归
    • 1.1 递归的思想
  • 二、递归的限制条件
  • 三、递归的举例
    • 举例1:求n的阶乘
      • 分析与代码的实现
    • 举例2:顺序打印一个整数的每一位
      • 分析与代码的实现
  • 四、递归与迭代
  • 举例3:求第n个斐波那契数
  • 五、结尾

一、什么是递归

函数递归是C语言中一种重要的编程技术,它允许一个函数在其定义中调用自身。 递归通常用于解决那些可以分解为类似子问题的问题,通过递归调用,函数能够逐步简化问题直到达到一个简单的情况(称为基准情形),从而解决整个问题。
递归其实是⼀种解决问题的方法,在C语言中,递归就是函数自己调用自己
来举一个简单的例子

#include<stdio.h>
int main()
{
	printf("xiaofeixiang");
	main();//main函数又调用的main函数
	return 0
}

在这里插入图片描述

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

栈溢出(Stack Overflow)是C语言编程中常见的一个问题,它发生在程序运行过程中,当往栈里存放的数据超过了栈所能容纳的最大容量时,就会导致程序出现异常行为

1.1 递归的思想

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

二、递归的限制条件

递归的基本要素
基准情形(Base Case)

递归函数必须有一个或多个基准情形,这些情形不需要递归调用就能直接解决。基准情形防止了无限递归。
也就是说:递归存在限制条件,当满足这个限制条件的时候,递归便不再继续

任何一次函数调用都会申请内存中栈区的资源,函数是可以递归的,但不能无限递归,因为栈区可能提供不了那么多资源去进行函数调用。

递归情形(Recursive Case)

递归函数在基准情形之外,通过调用自身来解决问题。每次递归调用通常会将问题规模缩小。
也就是说**每次递归调用之后越来越接近这个限制条件** 。

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

三、递归的举例

举例1:求n的阶乘

关于阶乘的介绍: 一个正整数的 阶乘(factorial)所有小于及等于该数的正整数的积 ,并且0的阶乘为1。 自然数n的阶乘写作n!。

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

分析与代码的实现

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

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

这样的思路就是把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解的。
当 n==0 的时候,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", ret);
	return 0;
}

运行结果(这里不考虑n太大的情况,n太大存在溢出):
在这里插入图片描述

可能很多人对这里还不太理解,我们来画个图:
在这里插入图片描述

我们再来举一个例子:

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

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

比如:
输入:1234 输出:1 2 3 4
输入:520 输出:5 2 0

分析与代码的实现

这个题目,放在我们面前,首先想到的是,怎么得到这个数的每一位呢?
1、如果n是一位数,n的每一位就是自己
2、如果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)

直到被打印的数字变成一位数的时候,就不需要再拆分,递归结束,这样做代码就会从最下面的1开始往上面打印
那么代码完成也就比较清楚:

我们首先来写自定义函数:

void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);

}

接下来我们来看代码演示:

#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(1234) 打印1234每一位,拆解为首先Print(123)打印123的每一位,再打印得到的4把Print(123) 打印123每⼀位,拆解为首先Print(12)打印12的每一位,再打印得到的3直到Print打印的是一位数,直接打印就行。

我们再来画图推演一下:
在这里插入图片描述

四、递归与迭代

我们来理解一下迭代的含义,其实说白了,迭代就是循环:
迭代(Iteration)
迭代是一种使用循环结构(如 for 循环、while 循环)来重复执行代码块的编程技术。迭代通常依赖于循环变量来追踪进度,并在满足特定条件时停止循环。

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

int fact(int n)
{
	if (n == 0)
		return 1;
	else
		return n * fact(n - 1);
}

factt函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。

在C语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow) 的问题。

所以如果不想使用递归,就得想其他的办法,通常就是迭代的方式(通常就是循环的方式)。
比如:计算 n 的阶乘,也是可以产生1~n的数字累计乘在一起的。

int test(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	return ret;
}

像这样,我们可以创建个ret变量,然后让他去与i相乘,而i在每次循环都会+1,直到小于我们输出的值,这样也可以实现一个数的阶乘

代码演示:

#include<stdio.h>

int test(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	return ret;
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	int b = test(a);
	printf("%d", b);
	return 0;

}

上述代码是能够完成任务,并且效率是比递归的方式更好的。
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。
当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

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

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

int test(int n)
{
	if (n <= 2)
		return 1;
	else
		return test(n - 1) + test(n - 2);
}

代码演示:

#include<stdio.h>
int test(int n)
{
	if (n <= 2)
		return 1;
	else
		return test(n - 1) + test(n - 2);
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	int b = test(a);
	printf("%d", b);
	return 0;
}

我们输入8的时候,我们可以很快得到结果
在这里插入图片描述

当我们n输入为80的时候,需要很长时间才能算出结果,
而且内存占用贼高:
在这里插入图片描述

这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的,那是为什么呢?

在这里插入图片描述
其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。我们可以测试:

#include<stdio.h>
int count = 0;
int test(int n)
{
	if (n == 3)
		count++;
	if (n <= 2)
		return 1;
	else
		return test(n - 1) + test(n - 2);
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	int b = test(a);
	printf("%d\n", b);
	printf("count = %d\n", count);
	return 0;
}

在这里插入图片描述
这里我们看到了,在计算第40个斐波那契数的时候,使用递归方式,第3个斐波那契数就被重复计算了39088169次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。
我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了。
这样就有下面的代码:

nt test(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

代码演示:

#include<stdio.h>
int test(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
int main()
{
	int a = 0;
	scanf("%d", &a);
	int b = test(a);
	printf("%d", b);
	return 0;
}

看看结果:

在这里插入图片描述
迭代的方式去实现这个代码,效率就要高出很多了。
有时候,递归虽好,但是也会引入一些问题,所以我们一定不要迷恋递归,适可而止就好。

五、结尾

这一课的内容就到这里了,下节课继续学习操作符的其他一些知识
如果内容有什么问题的话欢迎指正,有什么问题也可以问我!
在这里插入图片描述

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

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

相关文章

【Linux系统】—— 编译器 gcc/g++ 的使用

【Linux系统】—— 编译器 gcc/g 的使用 1 用 gcc 直接编译2 翻译环境2.1 预处理&#xff08;进行宏替换&#xff09;2.2 编译&#xff08;生成汇编&#xff09;2.3 汇编&#xff08;生成机器可识别代码&#xff09;2.4 链接2.5 记忆小技巧2.6 编译方式2.7 几个问题2.7.1 如何理…

【Unity3D】3D物体摆放、场景优化案例Demo

目录 PlaceManager.cs(放置管理类) Ground.cs(地板类) 和 GroundData.cs(地板数据类) 额外知识点说明 1、MeshFilter和MeshRenderer的Bounds区别 2、Gizmos 绘制一个平行于斜面的立方体 通过网盘分享的文件&#xff1a;PlaceGameDemo2.unitypackage 链接: https://pan.baid…

智能系统的感知和决策

智能系统在感知和决策过程中具备的关键能力表现在智能感知/自主判定上&#xff0c;下面可以从感知的本质、自主判断的含义及其在智能系统中的作用进行深入分析。 1、智能感知&#xff1a;信息获取与理解 智能感知是指智能系统通过传感器或其他数据采集手段获取环境中的信息&…

Spring 中的事件驱动模型

事件驱动的基本了解 事件模式也就是观察者模式&#xff0c;当一个对象改变的时候&#xff0c;所有依赖的对象都会收到一个通知。 Subject&#xff1a;抽象主题 Observer&#xff1a;具体主题 Concrete Subject&#xff1a;抽象观察者&#xff0c;在得到更新通知之后去更新自…

linux-FTP服务配置与应用

也许你对FTP不陌生&#xff0c;但是你是否了解FTP到底是个什么玩意&#xff1f; FTP 是File Transfer Protocol&#xff08;文件传输协议&#xff09;的英文简称&#xff0c;而中文简称为 “文传协议” 用于Internet上的控制文件的双向传输。同时&#xff0c;它也是一个应用程序…

linux-NFS网络共享存储服务配置

1.NFS服务原理 NFS会经常用到&#xff0c;用于在网络上共享存储&#xff0c;这样讲&#xff0c;你对NFS可能不太了解&#xff0c;举一个例子&#xff0c; 加入有三台机器A,B,C&#xff0c;它们需要访问同一个目录&#xff0c;目录中都是图片&#xff0c;传统的做法是把这些 图…

Jenkins 启动

废话 这一阵子感觉空虚&#xff0c;心里空捞捞的&#xff0c;总想找点事情做&#xff0c;即使这是一件微小的事情&#xff0c;空余时间除了骑车、打球&#xff0c;偶尔朋友聚会 … 还能干什么呢&#xff1f; 当独自一人时&#xff0c;究竟可以做点什么&#xff0c;填补这空虚…

消息队列篇--原理篇--Pulsar(Namespace,BookKeeper,类似Kafka甚至更好的消息队列)

Apache Pulusar是一个分布式、多租户、高性能的发布/订阅&#xff08;Pub/Sub&#xff09;消息系统&#xff0c;最初由Yahoo开发并开源。它结合了Kafka和传统消息队列的优点&#xff0c;提供高吞吐量、低延迟、强一致性和可扩展的消息传递能力&#xff0c;适用于大规模分布式系…

Python配置MITMPROXY中间人监听配置

1、安装python 环境&#xff0c;此处可以使用conda安装:conda create --name my_new_env python3.12 2、pip安装mitmproxy&#xff1a;pip install mitmproxy&#xff0c;安装后如果使用mitmproxy --version 成功返回结果&#xff0c;说明已经在环境变量路径中&#xff0c;如果…

Java-数据结构-二叉树习题(2)

第一题、平衡二叉树 ① 暴力求解法 &#x1f4da; 思路提示&#xff1a; 该题要求我们判断给定的二叉树是否为"平衡二叉树"。 平衡二叉树指&#xff1a;该树所有节点的左右子树的高度相差不超过 1。 也就是说需要我们会求二叉树的高&#xff0c;并且要对节点内所…

【网络原理】万字详解 HTTP 协议

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. HTTP 前置知识1.1 HTTP 是什么1.2 HTPP 协议应用场景1.3 HTTP 协议工作过程 2. HTTP 协议格式2.1 fiddler…

基于STM32的智能寝室控制系统设计(论文+源码)

1 .系统整体设计 通过需求分析&#xff0c;本设计基于STM32的智能寝室控制系统整体架构如图2.1所示&#xff0c;整系统利用DHT11温湿度传感器获取室内环境数据&#xff0c;并通过OLED显示&#xff0c;提供用户实时信息&#xff0c;火焰传感器和烟雾传感器用于监测火灾情况&…

日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件

日历热力图&#xff0c;月度数据可视化图表&#xff0c;vue组件 先看效果&#x1f447; 在线体验https://www.guetzjb.cn/calanderViewGraph/ 日历图简单划分为近一年时间&#xff0c;开始时间是 上一年的今天&#xff0c;例如2024/01/01 —— 2025/01/01&#xff0c;跨度刚…

铁电存储器FM25CL64B简介及其驱动编写(基于STM32 hal库)

铁电存储器FM25CL64B简介及其驱动编写&#xff08;基于STM32 hal库&#xff09; 文章目录 铁电存储器FM25CL64B简介及其驱动编写&#xff08;基于STM32 hal库&#xff09;前言一、FM25CL64B简介二、驱动代码1.头文件2.c文件 总结 前言 FM25CL64B是赛普拉斯cypress出品的一款铁…

基于微信小程序的科创微应用平台设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

HarmonyOS Next 最强AI智能辅助编程工具 CodeGenie介绍

随着大模型的兴起&#xff0c;在智能编码领域首先获得了应用。 市面上从Microsoft Copilot到国内阿里通义&#xff0c;字节marscode等&#xff0c;都提供了copilot方式的智能编码工具。HarmonyOS Next作为诞生一年的新事物&#xff0c;由于代码量和文档迭代原因&#xff0c;在智…

WPF2-1在xaml为对象的属性赋值.md

1. AttributeValue方式 1.1. 简单属性赋值1.2. 对象属性赋值 2. 属性标签的方式给属性赋值3. 标签扩展 (Markup Extensions) 3.1. StaticResource3.2. Binding 3.2.1. 普通 Binding3.2.2. ElementName Binding3.2.3. RelativeSource Binding3.2.4. StaticResource Binding (带参…

Appium(四)

一、app页面元素定位 1、通过id定位元素: resrouce-id2、通过ClassName定位&#xff1a;classname3、通过AccessibilityId定位&#xff1a;content-desc4、通过AndroidUiAutomator定位5、通过xpath定位xpath、id、class、accessibility id、android uiautomatorUI AutomatorUI自…

Windows图形界面(GUI)-QT-C/C++ - Qt List Widget详解与应用

公开视频 -> 链接点击跳转公开课程博客首页 -> ​​​链接点击跳转博客主页 目录 QListWidget概述 使用场景 常见样式 QListWidget属性设置 显示方式 (Display) 交互行为 (Interaction) 高级功能 (Advanced) QListWidget常见操作 内容处理 增加项目 删除项目…

Oracle 创建并使用外部表

目录 一. 什么是外部表二. 创建外部表所在的文件夹对象三. 授予访问外部表文件夹的权限3.1 DBA用户授予普通用户访问外部表文件夹的权限3.2 授予Win10上的Oracle用户访问桌面文件夹的权限 四. 普通用户创建外部表五. 查询六. 删除 一. 什么是外部表 在 Oracle 数据库中&#x…