java中的尾递归

1、基本概念

1)尾调用:

在计算机学里,尾调用是指一个函数里的最后一个动作是返回一个函数的调用结果的情形,即最后一步新调用的返回值直接被当前函数的返回结果。此时,该尾部调用位置被称为尾位置。尾调用中有一种重要而特殊的情形叫做尾递归。经过适当处理,尾递归形式的函数的运行效率可以被极大地优化。尾调用原则上都可以通过简化函数调用栈的结构而获得性能优化(称为“尾调用消除”),但是优化尾调用是否方便可行取决于运行环境对此类优化的支持程度如何。

2)尾递归:

若函数在尾位置调用自身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数。对尾递归的优化也是关注尾调用的主要原因。尾调用不一定是递归调用,但是尾递归特别有用,也比较容易实现。

尾递归在普通尾调用的基础上,多出了2个特征:

  1. 在尾部调用的是函数自身 (Self-called);
  2. 可通过优化,使得计算仅占用常量栈空间 (Stack Space)。
3)尾递归优化:

尾递归和一般的递归的不同点在对内存的占用,普通递归每次递归调用时回创建新的stack,从而stack膨胀,随着递归的结束而后收缩。而尾递归只会占用恒量的内存(和迭代一样)。看一个直观的例子:计算从1到n的累加(python)

def recsum(x):if x == 1:return xelse:return x + recsum(x - 1)

当调用recsum(5),Python调试器中发生如下状况:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

可以看到,随着递归调用的进行,从左到右达到顶峰,再从右到左收缩。而我们通常不希望这样的事情发生(当调用栈达到一定深度就会报statck overflow错),通常使用以下方法优化:

a.迭代:只占据常量stack space(更新这个栈!而非扩展他)。

for i in range(6):sum += i

b.尾递归优化:

def tailrecsum(x, running_total=0):if x == 0:return running_totalelse:return tailrecsum(x - 1, running_total + x)

虽然也是递归调用,但是这种写法可以被编译器优化,变成:

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

观察到,tailrecsum(x, y)中形式变量y的实际变量值是不断更新的,对比普通递归就很清楚,后者每个recsum()调用中y值不变,仅在层级上加深。所以,尾递归是把变化的参数传递给递归函数的变量了。

注:上面的第二种仅为了说明尾递归,实际上python的编译器时不支持尾递归优化的。

4)怎么写尾递归?

形式上只要最后一个return语句是单纯函数就可以。如:

return tailrec(x+1);

return tailrec(x+1) + x;

则不可以。因为无法更新tailrec()函数内的实际变量,只是新建一个栈。

5)尾递归优化原理:

传统模式的编译器对于尾调用的处理方式就像处理其他普通函数调用一样,总会在调用时创建一个新的栈帧(stack frame)并将其推入调用栈顶部,用于表示该次函数调用。

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。

2、java为什么不支持尾递归优化

Python,Java,Pascal等编译器没有实现尾递归优化(Tail Call Optimization, TCO),所以采用了for, while, goto等特殊结构代替recursive的表述。像C这类编译器,在语言层变一旦写成尾递归形式,就可以进行尾递归优化。

这里要强调一下:不是“Java没有尾递归优化”,而是“Java编译器没有尾递归优化”。这样更准确!

1)Java编译器本身应该是不太可能直接支持尾递归优化的:

Project Loom已经箭在弦上。Loom官方是这样介绍自己的:

Project Loom is to intended to explore, incubate and deliver Java VM features ... This is accomplished by the addition of the following constructs:

  • Virtual threads
  • Delimited continuations
  • Tail-call elimination

虽然协程的优先级更高,但既然选择了在JVM层面来做尾递归的路子,就不太可能在编译器层面再来一次。

2)尾递归优化会更改调用栈,对于程序员debug来说非常不方便:

以Kotlin为例,下面这段有bug的代码是没有开启尾递归优化的:

fun triangle(n: Int) {if (n > 0) {println("*".repeat(n))triangle(n-1)} else {println(1/0)    // <--- 在这里的递归边界刻意埋了一个bug}
}
fun main() {triangle(5)
}

报错信息:

Exception in thread "main" java.lang.ArithmeticException: / by zeroat MainKt.triangle(Main.kt:6)at MainKt.triangle(Main.kt:4)at MainKt.triangle(Main.kt:4)at MainKt.triangle(Main.kt:4)at MainKt.triangle(Main.kt:4)at MainKt.triangle(Main.kt:4)at MainKt.main(Main.kt:11)at MainKt.main(Main.kt)at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.base/java.lang.reflect.Method.invoke(Method.java:566)at org.jetbrains.kotlin.runner.AbstractRunner.run(runners.kt:64)at org.jetbrains.kotlin.runner.Main.run(Main.kt:176)at org.jetbrains.kotlin.runner.Main.main(Main.kt:186)

可以非常清晰地看到函数调用的情况,直接对应到程序员写的代码。

如果加上tailrec开启尾递归优化,代码倒是变动不大(添加tailrec关键字):

tailrec fun triangle(n: Int) {if (n > 0) {println("*".repeat(n))triangle(n-1)} else {println(1/0)}
}
fun main() {triangle(5)
}

错误信息变成了

Exception in thread "main" java.lang.ArithmeticException: / by zeroat MainKt.triangle(Main.kt:6)at MainKt.main(Main.kt:11)at MainKt.main(Main.kt)at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.base/java.lang.reflect.Method.invoke(Method.java:566)at org.jetbrains.kotlin.runner.AbstractRunner.run(runners.kt:64)at org.jetbrains.kotlin.runner.Main.run(Main.kt:176)at org.jetbrains.kotlin.runner.Main.main(Main.kt:186)

可以看到,triangle只有一层调用。这是因为编译器做了尾递归优化,把递归调用展开成了循环。运行效率虽然提高了,但是程序员调试的时候很容易一头雾水。这还是最简单的递归。假如A尾调用B、B尾调用A呢?理论上是可以优化的,但出了bug程序员简直要抓瞎。而Java作为一个相对高级的语言,尤其是有了未来JVM的支持,基本没必要在编译器层面自己给自己找麻烦。

3)JVM尚未支持尾递归优化,更主要是历史遗留问题

Brian Goetz(Java语言首席架构师)曾经在一个演讲中解释过。

简而言之,JDK里面过去有一些诡异的安全机制,会去数当前调用栈究竟有多少个帧。如果帧数不对,就会报错。然而尾递归优化就是要减少栈帧好么!这不没事找事吗?这种安全机制显然是一种dirty hack,然而屎山一旦留下就覆水难收,直接导致这么多年压根没法在JVM层面搞尾递归优化。Brian并没有明确指出究竟是哪块代码,不过我找到了一篇貌似相关的论文,感兴趣的可以研究一下。当然,现如今这块屎山已经移除了,所以JVM的尾递归优化应该是指日可待了。

为什么Java编译器没有实现尾递归优化? - 知乎

示例:看一个累加计算的例子:

 

private static int sum(int n) {if (n == 1) {return 1;}return n + sum(n-1);
}

当n=19000的时候就会出现栈溢出。接下来,进行尾递归优化:

private static int sum2(int n, int sumRes) {if (n == 0) {return sumRes;}return sum2(n-1, n + sumRes);
}

当n=19000的时候继续执行,发现仍然会出现栈溢出。所以可以证明,虽然形式上写成了尾递归,但是java编译器并没有对其进行优化。

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

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

相关文章

[翻译] 在 CI 或测试环境中使用 Docker-in-Docker,三思而后行

发布日期&#xff1a;2024-04-08 18:01:01 原文地址&#xff1a;Using Docker-in-Docker for your CI or testing environment? Think twice. Docker-in-Docker 的主要目的是帮助 Docker 本身的开发。许多人使用它来运行 CI&#xff08;例如使用 Jenkins&#xff09;&#xf…

[NKCTF2024]-PWN:leak解析(中国剩余定理泄露libc地址,汇编覆盖返回地址)

查看保护 查看ida 先放exp 完整exp&#xff1a; from pwn import* from sympy.ntheory.modular import crt context(log_leveldebug,archamd64)while True:pprocess(./leak)ps[101,103,107,109,113,127]p.sendafter(bsecret\n,bytes(ps))cs[0]*6for i in range(6):cs[i]u32(p…

Java 基于微信小程序的校园请教小程序的研究与实现,附源码

博主介绍&#xff1a;✌程序员徐师兄、10年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447…

SpringBoot整合Spring Data JPA

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉🍎个人主页:Leo的博客💞当前专栏: 循序渐进学SpringBoot ✨特色专栏: MySQL学习 🥭本文内容: SpringBoot整合Spring Data JPA 📚个人知识库: Leo知识库,欢迎大家访问 1.…

ChatGPT新手指南:如何用AI写出专业学术论文

ChatGPT无限次数:点击直达 ChatGPT新手指南&#xff1a;如何用AI写出专业学术论文 在当今信息爆炸的时代&#xff0c;人工智能技术的快速发展为我们提供了许多新的可能性。ChatGPT作为一种先进的自然语言处理技术&#xff0c;不仅能够进行对话和文本生成&#xff0c;还可以辅助…

淘宝销量API商品详情页原数据APP接口测试㊣

淘宝/天猫获得淘宝app商品详情原数据 API 返回值说明 item_get_app-获得淘宝app商品详情原数据 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地…

Java-StringBuilder容器

一、基础用法 1.创建对象 StringBuilder sbnew StringBuilder(); 2.添加元素 可以添加整型、浮点型、字符串等。 sb.append(1); sb.append(2.3); sb.append(true); 3.反转 sb.reverse(); 4.获取长度 int len sb.length(); 5.转变成字符串 tring strsb.toString(); …

Python学习笔记11 - 列表

1. 列表的创建与删除 2. 列表的查询操作 3. 列表的增、删、改操作 4. 列表元素的排序 5. 列表生成式

利用IP地址判断羊毛用户:IP数据云提供IP风险画像

在当今数字化社会&#xff0c;互联网已经成为人们日常生活和商业活动中不可或缺的一部分。然而&#xff0c;随着网络的普及&#xff0c;网络欺诈行为也日益猖獗&#xff0c;其中包括了羊毛党这一群体。羊毛党指的是利用各种手段获取利益、奖励或者优惠而频繁刷取优惠券、注册账…

png转换成jpg格式?这几种方法很简单

在发送电子邮件时&#xff0c;附件的大小是一个重要的考虑因素。将PNG图像转换为jpg格式可以减小文件大小&#xff0c;减少附件的传输时间和存储空间占用。这对于商务邮件、个人邮件或邮件营销活动中的图片附件都非常有用&#xff0c;下面就介绍几个可以快速完成图片转格式的方…

C++之std::initializer_list详解

目录 1.引言 2.容器的初始化 3.函数中使用std::initializer_list 4.自定义类型中使用std::initializer_list 5.迭代std::initializer_list 6. 在模板中使用std::initializer_list 7.std::initializer_list的限制 8.总结 1.引言 std::initializer_list 是 C11 中的一个特…

在Win11上部署大模型推理加速工具vLLM

vLLM是伯克利大学LMSYS组织开源的大语言模型高速推理框架&#xff0c;旨在极大地提升实时场景下的语言模型服务的吞吐与内存使用效率。vLLM是一个快速且易于使用的库&#xff0c;用于 LLM 推理和服务&#xff0c;可以和HuggingFace 无缝集成。vLLM利用了全新的注意力算法PagedA…

Docker容器与虚拟化技术:OpenEuler 部署 Prometheus 与 Grafana

目录 一、实验 1.环境 2.OpenEuler 部署 Prometheus 3.OpenEuler 部署 Grafana 4.使用cpolar内网穿透 二、问题 1.拉取镜像失败 2.如何导入Grafana监控模板&#xff08;ES&#xff09; 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 系统架构版本IP备注…

Scrapy框架spider类异常处理

说明&#xff1a;仅供学习使用&#xff0c;请勿用于非法用途&#xff0c;若有侵权&#xff0c;请联系博主删除 作者&#xff1a;zhu6201976 一、捕获Request所有网络相关异常 在spider类中&#xff0c;我们构造Request对象或FormRequest对象时&#xff0c;可传递参数errback回调…

JAR包文件修改

项目中遇到修改JAR包中的某个依赖或者配置的话一般分为两种方式 1、压缩文件只能用来修改&#xff08;1&#xff09;配置文件&#xff08;2&#xff09;Java类&#xff08;3&#xff09;Mapper文件 2、使用Java 命令来 &#xff08;1&#xff09;解压Jar包 &#xff08;2&…

BugKu:Simple SSTI

1.进入此题 2.查看源代码 可以知道要传入一个名为flag的参数&#xff0c;又说我们经常设置一个secret_key 3.flask模版注入 /?flag{{config.SECRET_KEY}} 4.学有所思 4.1 什么是flask&#xff1f; flask是用python编写的一个轻量web开发框架 4.2 SSTI成因&#xff08;SST…

【数据结构与算法】:堆排序和选择排序

1. 堆排序 堆排序是一种比较复杂的排序算法&#xff0c;因为它的流程比较多&#xff0c;理解起来不会像冒泡排序和选择排序那样直观。 1.1 堆的结构 要理解堆排序&#xff0c;首先要理解堆。堆的逻辑结构是一棵完全二叉树&#xff0c;物理结构是一个数组。 (如果不知道什么是…

链表的中间结点——每日一题

题目链接&#xff1a; OJ链接 题目&#xff1a; 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5] 解释&…

sql注入笔记整理

概念 发生在与数据库交互时&#xff1b;将未经过滤的用户输入信息合并到执行代码中造成恶意代码的执行错误注入 extractvalue(xml_flag,xpath) 如果出错打印xpath内容&#xff1b;数据库不识别#~符号 extractvalue(1,concat(~,database(),~)) 会打印出~数据库名~ updatexml(xml…

【架构师】-- 成长路线图

成长为软件架构师不是一件容易的事&#xff0c;这篇文章列举了架构师需要学习的技术储备&#xff0c;给出了成为软件架构师的路线图&#xff0c;帮助有志于在架构领域成长的同学可以明确学习的方向。原文&#xff1a;Master Plan for becoming a Software Architect[1] 软件架…