说说尾递归

article/2025/10/4 15:20:19

原文:https://www.cnblogs.com/catch/p/3495450.html

微博上看到有人在讨论尾递归,想起以前曾看过老赵写的一篇相关的博客,介绍的比较详细了,相信很多人都看过,我也在下面留了言,但挑了个刺,表示文章在关键点上一带而过了,老赵自然是懂的,但看的人如果不深入思考,未必真正的明白,下面我说说我的理解。

什么是尾递归

什么是尾递归呢?(tail recursion), 顾名思议,就是一种“不一样的”递归,说到它的不一样,就得先说说一般的递归。对于一般的递归,比如下面的求阶乘,教科书上会告诉我们,如果这个函数调用的深度太深,很容易会有爆栈的危险。

复制代码

// 先不考虑溢出问题
int func(int n)
{if (n <= 1) return 1;return (n * func(n-1));
}

复制代码

原因很多人的都知道,让我们先回顾一下函数调用的大概过程:

1)调用开始前,调用方(或函数本身)会往栈上压相关的数据,参数,返回地址,局部变量等。

2)执行函数。

3)清理栈上相关的数据,返回。

因此,在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C.... 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满 了,这就是一般递归函数所容易面临的大问题。

而尾递归在某些语言的实现上,能避免上述所说的问题,注意是某些语言上,尾递归本身并不能消除函数调用栈过长的问题,那什么是尾递归呢?在上面写的一般递归函数 func() 中,我们可以看到,func(n)  是依赖于 func(n-1) 的,func(n) 只有在得到 func(n-1) 的结果之后,才能计算它自己的返回值,因此理论上,在 func(n-1) 返回之前,func(n),不能结束返回。因此func(n)就必须保留它在栈上的数据,直到func(n-1)先返回,而尾递归的实现则可以在编译器的帮助下,消除这个限制:

复制代码

// 先不考虑溢出int tail_func(int n, int res)
{if (n <= 1) return res;return tail_func(n - 1, n * res);
}// 像下面这样调用tail_func(10000000000, 1);

复制代码

从上可以看到尾递归把返回结果放到了调用的参数里。这个细小的变化导致,tail_func(n, res)不必像以前一样,非要等到拿到了tail_func(n-1, n*res)的返回值,才能计算它自己的返回结果 -- 它完全就等于tail_func(n-1, n*res)的返回值。因此理论上:tail_func(n)在调用tail_func(n-1)前,完全就可以先销毁自己放在栈上的东西。

这就是为什么尾递归如果在得到编译器的帮助下,是完全可以避免爆栈的原因:每一个函数在调用下一个函数之前,都能做到先把当前自己占用的栈给先释放了,尾递归的调用链上可以做到只有一个函数在使用栈,因此可以无限地调用!

尾递归的调用栈优化特性

相信读者都注意到了,我一直在强调,尾递归的实现依赖于编译器的帮助(或者说语言的规定),为什么这样说呢?先看下面的程序:

复制代码

 1 #include <stdio.h>2 3 int tail_func(int n, int res)4 {5      if (n <= 1) return res;6 7      return tail_func(n - 1, n * res);8 }9 
10 
11 int main()
12 {
13     int dummy[1024*1024]; // 尽可能占用栈。
14     
15     tail_func(2048*2048, 1);
16     
17     return 1;
18 }

复制代码

上面这个程序在开了编译优化和没开编译优化的情况下编出来的结果是不一样的,如果不开启优化,直接 gcc -o tr func_tail.c 编译然后运行的话,程序会爆栈崩溃,但如果开优化的话:gcc -o tr -O2 func_tail.c,上面的程序最后就能正常运行。 

这里面的原因就在于,尾递归的写法只是具备了使当前函数在调用下一个函数前把当前占有的栈销毁,但是会不会真的这样做,是要具体看编译器是否最终这样做,如果在语言层面上,没有规定要优化这种尾调用,那编译器就可以有自己的选择来做不同的实现,在这种情况下,尾递归就不一定能解决一般递归的问题。

我们可以先看看上面的例子在开优化与没开优化的情况下,编译出来的汇编代码有什么不同,首先是没开优化编译出来的汇编tail_func:

复制代码

 1 .LFB3:2         pushq   %rbp3 .LCFI3:4         movq    %rsp, %rbp5 .LCFI4:6         subq    $16, %rsp7 .LCFI5:8         movl    %edi, -4(%rbp)9         movl    %esi, -8(%rbp)
10         cmpl    $1, -4(%rbp)
11         jg      .L4
12         movl    -8(%rbp), %eax
13         movl    %eax, -12(%rbp)
14         jmp     .L3
15 .L4:
16         movl    -8(%rbp), %eax
17         movl    %eax, %esi
18         imull   -4(%rbp), %esi
19         movl    -4(%rbp), %edi
20         decl    %edi
21         call    tail_func
22         movl    %eax, -12(%rbp)
23 .L3:
24         movl    -12(%rbp), %eax
25         leave
26         ret

复制代码

注意上面标红色的一条语句,call 指令就是直接进行了函数调用,它会先压栈,然后再 jmp 去 tail_func,而当前的栈还在用!就是说,尾递归的作用没有发挥。

再看看开了优化得到的汇编:

复制代码

 1 tail_func:2 .LFB13:3         cmpl    $1, %edi4         jle     .L85         .p2align 4,,76 .L9:7         imull   %edi, %esi8         decl    %edi9         cmpl    $1, %edi
10         jg      .L9
11 .L8:
12         movl    %esi, %eax
13         ret

复制代码

注意第7,第10行,尤其是第10行!tail_func() 里面没有函数调用!它只是把当前函数的第二个参数改了一下,直接就又跳到函数开始的地方。此处的实现本质其实就是:下一个函数调用继续延用了当前函数的栈!

这就是尾递归所能带来的效果: 控制栈的增长,且减少压栈,程序运行的效率也可能更高!

上面所写的是 c 的实现,正如前面所说的,这并不是所有语言都摆支持,有些语言,比如说 python, 尾递归的写法在 python 上就没有任何作用,该爆的时候还是会爆。

复制代码

def func(n, res):if (n <= 1):return resreturn func(n-1, n*res)if __name__ =='__main__':print func(4096, 1)

复制代码

不仅仅是 python,据说 C# 也不支持,我在网上搜到了这个链接:https://connect.microsoft.com/VisualStudio/feedback/details/166013/c-compiler-should-optimize-tail-calls,微软的人在上面回答说,实现这个优化有些问题需要处理,并不是想像中那么容易,因此暂时没有实现,但是这个回答是在2007年的时候了,到现在岁月变迁,不知支持了没?我看老赵写的尾递归博客是在2009年,用 c# 作的例子,估计现在 c# 是支持这个优化的了(待考).

尾调用

前面的讨论一直都集中在尾递归上,这其实有些狭隘,尾递归的优化属于尾调用优化这个大范畴,所谓尾调用,形式它与尾递归很像,都是一个函数内最后一个动作是调用下一个函数,不同的只是调用的是谁,显然尾递归只是尾调用的一个特例。

复制代码

int func1(int a)
{static int b = 3;return a + b;
}int func2(int c)
{static int b = 2;return func1(c+b);
}

复制代码

上面例子中,func2在调用func1之前显然也是可以完全丢掉自己占有的栈空间的,原因与尾递归一样,因此理论上也是可以进行优化的,而事实上这种优化也一直是程序编译优化里的一个常见选项,甚至很多的语言在标准里就直接要求要对尾调用进行优化,原因很明显,尾调用在程序里是经常出现的,优化它不仅能减少栈空间使用,通常也能给程序运行效率带来比较大的提升。 


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

相关文章

Kotlin尾递归优化

Kotlin尾递归优化 尾调用&#xff08;Tail Call&#xff09;是函数式编程的一个重要概念&#xff0c;本文介绍它的含义和用法。 1. 尾递归 ​ 如果一个函数中所有递归形式的调用都出现在函数的末尾&#xff0c;我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执…

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

在计算机科学领域中&#xff0c;递归式通过递归函数来实现的。程序调用自身的编程技巧称为递归( recursion)。 一个过程或者函数在其定义或者说明中有直接或者间接调用自身的一种方法&#xff0c;它通常把一个大型复杂的问题层层转化为一个与原问题类似的规模较小的问题来求解&…

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…