c语言尾递归,C语言——递归与尾递归

article/2025/10/4 16:07:33

在计算机科学领域中,递归式通过递归函数来实现的。程序调用自身的编程技巧称为递归( recursion)。

一个过程或者函数在其定义或者说明中有直接或者间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解,递归策略只要一些的程序即可形容出解题过程所需要的屡次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。

一般来说,递归需要有:边界条件、递归前进段和递归返回段。

当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

注意:

(1) 递归就是在过程或者函数里调用自身;

(2) 在使用递归策略时,必需有一个明确的递归结束条件,称为递归出口。

基本递归

问题:计算n!

数学上的计算公式为:n!=n×(n-1)×(n-2)……2×1

使用递归的方式,可以定义为:

b632b21f709d33f70b62d8c3066748eb.png有兴趣一起学习交流C语言的小伙伴可以加群:941636044

以递归的方式计算4!

F(4)=4×F(3)            递归阶段

F(3)=3×F(2)

F(2)=2×F(1)

F(1)=1  终止条件

F(2)=(2)×(1)    回归阶段

F(3)=(3)×(2)

F(4)=(4)×(6)

24                 ?递归完成

以递归方式实现阶乘函数的实现:int fact(int n) {

????if(n < 0)

????????return 0;

????else if (n == 0 || n == 1)

????????return 1;

????else

????????return n * fact(n - 1);

}

下面来详细分析递归的工作原理

先看看C语言中函数的执行方式,需要理解少量关于C程序在内存中的组织方式:

BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

代码段:代码段(code segment/text segment)通常是指用来存放?程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读?, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含少量只读的常数变量?,例如字符串常量等。程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或者缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态增加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)

栈(stack):栈又称堆栈, 存放程序的局部变量(但不包括static公告的变量,?static?意味着?在数据段中存放变量)。除此以外,在函数被调用时,栈用来传递参数和返回值。因为栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

堆的增长方向为从低地址到高地址向上增长,而栈的增长方向恰好相反(实际情况与CPU的体系结构有关)

当C程序中调用了一个函数时,栈中会分配一块空间来保存与这个调用相关的信息,每一个调用都被当作是活跃的。栈上的那块存储空间称为活跃记录或者者栈帧

栈帧由5个区域组成:输入参数、返回值空间、计算表达式时用到的临时存储空间、函数调用时保存的状态信息以及输出参数,参见下图:

5632dcd66765e6b645e032d88f867185.png有兴趣一起交流学习c/c++的小伙伴可以加群:941636044

可以使用下面的程序来检验:#include

int g1=0, g2=0, g3=0;

int max(int i)

{

????int m1 = 0, m2, m3 = 0, *p_max;

????static n1_max = 0, n2_max, n3_max = 0;

????p_max = (int*)malloc(10);

????printf("打印max程序地址\n");

????printf("in max: 0x%08x\n\n",max);

????printf("打印max传入参数地址\n");

????printf("in max: 0x%08x\n\n",&i);

????printf("打印max函数中静态变量地址\n");

????printf("0x%08x\n",&n1_max); //打印各本地变量的内存地址

????printf("0x%08x\n",&n2_max);

????printf("0x%08x\n\n",&n3_max);

????printf("打印max函数中局部变量地址\n");

????printf("0x%08x\n",&m1); //打印各本地变量的内存地址

????printf("0x%08x\n",&m2);

????printf("0x%08x\n\n",&m3);

????printf("打印max函数中malloc分配地址\n");

????printf("0x%08x\n\n",p_max); //打印各本地变量的内存地址

????if(i) return 1;

????else return 0;

}

int main(int argc, char **argv)

{

????static int s1=0, s2, s3=0;

????int v1=0, v2, v3=0;

????int *p;????

????p = (int*)malloc(10);

????printf("打印各全局变量(已初始化)的内存地址\n");

????printf("0x%08x\n",&g1); //打印各全局变量的内存地址

????printf("0x%08x\n",&g2);

????printf("0x%08x\n\n",&g3);

????printf("======================\n");

????printf("打印程序初始程序main地址\n");

????printf("main: 0x%08x\n\n", main);

????printf("打印主参地址\n");

????printf("argv: 0x%08x\n\n",argv);

????printf("打印各静态变量的内存地址\n");

????printf("0x%08x\n",&s1); //打印各静态变量的内存地址

????printf("0x%08x\n",&s2);

????printf("0x%08x\n\n",&s3);

????printf("打印各局部变量的内存地址\n");

????printf("0x%08x\n",&v1); //打印各本地变量的内存地址

????printf("0x%08x\n",&v2);

????printf("0x%08x\n\n",&v3);

????printf("打印malloc分配的堆地址\n");

????printf("malloc: 0x%08x\n\n",p);

????printf("======================\n");

????max(v1);

????printf("======================\n");

????printf("打印子函数起始地址\n");

????printf("max: 0x%08x\n\n",max);

????return 0;

}

栈是用来存储函数调用信息的绝好方案,然而栈也有少量缺点:

栈维护了每个函数调用的信息直到函数返回后才释放,这需要占用相当大的空间,尤其是在程序中使用了许多的递归调用的情况下。除此之外,由于有大量的信息需要保存和恢复,因而生成和销毁活跃记录需要消耗肯定的时间。我们需要考虑采用迭代的方案。幸运的是我们可以采用一种称为尾递归的特殊递归方式来避免前面提到的这些缺点。

尾递归

定义

假如一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,由于大多数现代的编译器会利用这种特点自动生成优化的代码。

原理

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创立一个新的。编译器可以做到这点,由于递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其余事情可做,因而也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新增一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。尽管编译器能够优化尾递归造成的栈溢出问题,但是在编程中,我们还是应该尽量避免尾递归的出现,由于所有的尾递归都是可以用简单的goto循环替代的。

实例

为了了解尾递归是如何工作的,让我们再次以递归的形式计算阶乘。首先,这可以很容易让我们了解为什么之前所定义的递归不是尾递归。回忆之前对计算n!的定义:在每个活跃期计算n倍的(n-1)!的值,让n=n-1并持续这个过程直到n=1为止。这种定义不是尾递归的,由于每个活跃期的返回值都依赖于用n乘以下一个活跃期的返回值,因而每次调用产生的栈帧将不得不保存在栈上直到下一个子调用的返回值确定。现在让我们考虑以尾递归的形式来定义计算n!的过程。

这种定义还需要接受第二个参数a,除此之外并没有太大区别。a(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令a=na并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回a就可。

代码实例给出了一个C函数facttail,它接受一个整数n并以尾递归的形式计算n!。这个函数还接受一个参数a,a的初始值为1。facttail使用a来维护递归层次的深度,除此之外它和fact很类似。读者可以注意一下函数的具体实现和尾递归定义的类似之处。int facttail(int n, int a)

{

????if (n < 0)

????????return 0;

????else if (n == 0)

????????return 1;

????else if (n == 1)

????????return a;

????else

????????return facttail(n - 1, n * a);

}

示例中的函数是尾递归的,由于对facttail的单次递归调用是函数返回前最后执行的一条语句。在facttail中碰巧最后一条语句也是对facttail的调用,但这并不是必须的。换句话说,在递归调用之后还可以有其余的语句执行,只是它们只能在递归调用没有执行时才可以执行。

尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比方f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈就可,之前的可优化删去。

也许在C语言中有很多的特例,但编程语言不只有C语言,在函数式语言Erlang中(亦是栈语言),假如想要保持语言的高并发特性,就必需用尾递归来替代传统的递归。


http://chatgpt.dhexx.cn/article/UUKwW9JY.shtml

相关文章

php 尾递归,又见尾递归

这几天看到几篇关于尾递归的文章&#xff0c;之前对尾递归没有多大概念&#xff0c;所以回头研究了一下尾递归。 尾递归的概念 尾递归(Tail Recursion)的概念是递归概念的一个子集。对于普通的递归&#xff0c;由于必须要记住递归的调用堆栈&#xff0c;由此产生的耗用是难以估…

javascript尾递归优化

JS中的递归 我们来看一个阶乘的代码 function foo( n ){if(n < 1){return 1;}return n * foo( n - 1 ); }foo(5); // 120下面分析一下&#xff0c;代码运行过程中,执行上下文栈是怎么变化的 这个代码是在全局作用域中执行的&#xff0c;所以在foo函数得到执行之前&#x…

Scala尾递归

一、首先来简单介绍一下递归和尾递归 1.递归&#xff1a; 简单来说就是在函数内部调用函数本身来完成函数体。对于返回值的要求并不很严格。 递归的缺点&#xff1a;递归效率比较低&#xff0c;调用次数过多还会出现栈溢出的问题。 2.尾递归&#xff1a; 尾递归的核心思想&…

java 递归 尾递归_递归和尾递归

C允许一个函数调用其本身&#xff0c;这种调用过程被称作递归(recursion)。 最简单的递归形式是把递归调用语句放在函数结尾即恰在return语句之前。这种形式被称作尾递归或者结尾递归&#xff0c;因为递归调用出现在函数尾部。由于为递归的作用相当于一条循环语句&#xff0c;所…

递归算法和尾递归

一、递归算法 1、递归算法思想 递归算法是在程序中不断反复调用&#xff08;间接或直接地调用&#xff09;自身&#xff0c;来达到解决问题的算法。 递归是一个方法在其方法体内调用自身方法的调用方式。递归调用分为两种情况&#xff1a; 直接递归&#xff0c;即在方法中调用…

尾递归(Tail Recursion)

向上递归(Pass-Up Recursion) 先来看一下传统的递归 def sum(lst):"""把lst所有元素求和"""if len(lst) 0:return 0return lst[0] sum(lst[1:])加入sum([1,2,3]),则是先求sum([2,3])&#xff1b;而要求sum([2,3]),则要求sum([3]), 求sum([3]…

尾递归

什么是尾递归 什么是尾递归呢?(tail recursion), 顾名思议&#xff0c;就是一种“不一样的”递归&#xff0c;说到它的不一样&#xff0c;就得先说说一般的递归。对于一般的递归&#xff0c;比如下面的求阶乘&#xff0c;教科书上会告诉我们&#xff0c;如果这个函数调用的深…

【算法】递归:递归优化之尾递归

【算法】递归&#xff1a;递归优化之尾递归 引言&#xff1a;在以往我发过一篇过于通过分析法去理解递归求解递归的博客文章&#xff0c;那篇文章主要介绍了如何去求解递归问题。而在这篇文章中&#xff0c;我会介绍一下如何去优化递归&#xff0c;顺带还会去分析一下递归算法的…

DDD微服务架构设计第四课 DDD指导微服拆分和落地实现

07 在线订餐场景中是如何开事件风暴会议的&#xff1f; 微服务设计最核心的难题是微服务的拆分&#xff0c;不合理的微服务拆分不仅不能提高研发效率&#xff0c;反倒还使得研发效率更低&#xff0c;因此要讲究“小而专”的设计。“小而专”的设计意味着微服务的设计不是简单拆…

微服架构的论述?

搭建微服架构 一、什么是微服架构 简单的说就是将一个整体的应用按照一定的规则拆分成一个个独立的应用&#xff0c;这些独立的应用后面又组合成了一个整体的应用。 比如说一个博客系统&#xff0c;我可能包含了发表文章&#xff0c;用户登录&#xff0c;用户评论等功能&…

微服务架构是什么?

微服务架构是什么&#xff1f; 一文详解微服务架构最初的需求随着业务发展……是时候做出改变了没有银弹监控 - 发现故障的征兆定位问题 - 链路跟踪分析问题 - 日志分析网关 - 权限控制&#xff0c;服务治理服务注册于发现 - 动态扩容熔断、服务降级、限流熔断服务降级限流 测试…

SpringCloud微服之Nacos的学习

1:使用前提 第一步:解压启动Nocos SpringCloudAlibaba 推出了一个名为 Nacos 的注册中心&#xff0c;在国外也有大量的使用。 startup.cmd -m standalone 访问http://localhost:8848/nacos/ 第二步:服务注册 工程目录 在父工程中添加依赖 <dependency><groupI…

python注册nacos微服并使用gateway网关

业务需求&#xff1a;使用python flask框架和java spring boot框架共同注册到nacos中&#xff0c;在由spring cloud gateway分配路由。 flaskDome: from flask import Flaskapp Flask(__name__)app.route(/python) def test():return "这是python flask框架接口&#xf…

大开眼界,Jenkins结合SpringCloud+K8S,打通微服一条龙技术讲解

Jenkins 是目前最常用的持续集成工具&#xff0c;拥有近50%的市场份额&#xff0c;他还是很多技术团队的第一个使用的自动化工具。由此可见他的重要性&#xff01; 这份Jenkins宝典从入门介绍到结合DockerSpringCloudKubernetes&#xff0c;打通一条龙技术讲解&#xff0c;简直…

基于Ant DesignPro Vue + SpringBoot 前后端分离 - 后端微服化 + 接口网关 + Nacos

基于Ant DesignPro Vue SpringBoot 前后端分离 - 后端微服化 接口网关 Nacos 通过Ant DesignPro Vue SpringBoot 搭建的后台管理系统后&#xff0c;实现了前后端分离&#xff0c;并实现了登录认证&#xff0c;认证成功后返回该用户相应权限范围内可见的菜单。 后端采用Spri…

微服务消费端通过feign调用微服异常问题

在项目开发中,我们的调用方通过Feign调用微服时,如果微服出现业务异常(例如空指针,或抛出自定义的异常)和非业务异常(参数不合法4xx异常)都会进入到调用方的全局异常拦截器,抛出的code全部转换成了500,这样不友好 实际上只有业务异常feign才会转换成500错误且转成FeignExcepti…

大开眼界!Jenkins结合SpringCloud+K8S,打通微服一条龙技术讲解

Jenkins 是目前最常用的持续集成工具&#xff0c;拥有近50%的市场份额&#xff0c;他还是很多技术团队的第一个使用的自动化工具。由此可见他的重要性&#xff01; 这份Jenkins宝典从入门介绍到结合DockerSpringCloudKubernetes&#xff0c;打通一条龙技术讲解&#xff0c;简直…

从0搭建公司SpringCloud Alibaba 微服務鉴权中心服务(详细教程)

大家好&#xff0c;我是宝哥&#xff01; 鉴权中心服务 认识JWT json web token 是一个开放的标准 &#xff0c;它定义了一个种紧凑的&#xff0c;自包含的方式&#xff0c;用于作为json对象在各方之间安全的传输信息 服务器鉴权完成之后 会生成 json 对象 发送给客户端&#x…

腾讯云开源业界微服管理框架 Femas

导读 企业数字化向云原生演进过程面临诸多痛点&#xff0c;微服务框架不统一、协议多样化、语言异构&#xff0c;纷繁复杂的微服务技术栈&#xff0c;基础组件之间像一座座孤岛&#xff0c;各个基础组件的控制面不能互联&#xff0c;让用户的体验非常割裂&#xff0c;各种历史包…

基于Ant DesignPro Vue + SpringBoot 前后端分离 - 后端微服化 + 接口网关 + Nacos + Sentinel

基于Ant DesignPro Vue SpringBoot 前后端分离 - 后端微服化 接口网关 Nacos Sentinel 通过Ant DesignPro Vue SpringBoot 搭建的后台管理系统后&#xff0c;实现了前后端分离&#xff0c;并实现了登录认证&#xff0c;认证成功后返回该用户相应权限范围内可见的菜单。 后…