java 递归 堆栈_Java中的堆栈安全递归

java 递归 堆栈

stack_safe_recursion_in_Java_ebook 在本文中,摘自《 Java中的函数编程 》一书,我解释了如何使用递归,同时避免了StackOverflow异常的风险。

Corecursion正在使用第一步的输出作为下一步的输入来构成计算步骤。 递归是相同的操作,但是从最后一步开始。 在这种情况下,我们必须延迟评估,直到遇到基本条件(与corecursion的第一步相对应)为止。

假设我们的编程语言中只有两条指令:递增(向值加1)和递减(从值中减去1)。 让我们通过编写这些指令来实现加法。

Corecursive和递归加法示例

为了将两个数字x和y相加,我们可以执行以下操作:

  • 如果y == 0 ,则返回x
  • 否则,递增x ,递减y ,然后重新开始。

这可以用Java编写为:

static int add(int x, int y) {while(y > 0) {x = ++x;y = --y;}return x;
}

或更简单:

static int add(int x, int y) {while(y-- > 0) {x = ++x;}return x;
}

注意,直接使用参数xy没问题,因为在Java中,所有参数都是按值传递的。 还要注意,我们使用后减量来简化编码。 但是,我们可以通过稍微改变条件来使用预减量,从而将形式从y迭代为1到将y‑1迭代为0

static int add(int x, int y) {while(--y >= 0) {x = ++x;}return x;
}

递归版本比较棘手,但仍然非常简单:

static int addRec(int x, int y) {return y == 0? x: addRec(++x, --y);
}

两种方法似乎都可行,但是如果我们尝试使用大量的递归版本,可能会感到惊讶。 虽然

addRec(10000, 3);

生成期望结果10003,切换参数,如下所示:

addRec(3, 10000);

产生一个StackOverflowException

如何用Java实现递归?

要了解正在发生的事情,我们必须查看Java如何处理方法调用。 调用方法时,Java会挂起当前正在执行的操作,并将环境压入堆栈以为调用的方法执行留出空间。 当此方法返回时,Java将弹出堆栈以恢复环境并恢复程序执行。 如果我们依次调用一个方法,则堆栈将始终保存这些方法调用环境中的至少一个。

但是方法不仅是通过一个接一个地调用它们而构成的。 方法调用方法。 如果method1作为其实现的一部分调用method2 ,则Java会再次挂起method1执行,将当前环境压入stack ,然后开始执行method2 。 当method2返回时,Java从堆栈中弹出最后推送的环境并恢复执行(在本例中为method1 )。 method1完成后,Java再次从堆栈中弹出,并恢复调用该方法之前的操作。

当然,方法调用可能嵌套得很深。 方法嵌套深度是否有限制? 是。 限制是堆栈的大小。 在当前情况下,该限制大约在几千个级别,尽管可以通过配置堆栈大小来增加此限制。 但是,所有线程都使用相同的堆栈大小,因此增加单个计算的堆栈大小通常会浪费空间。 默认堆栈大小在320k和1024k之间变化,具体取决于Java版本和所使用的系统。 对于具有最小堆栈使用率的64位Java 8程序,嵌套方法调用的最大数量约为7000。通常,除了非常特殊的情况外,我们不需要更多的嵌套方法调用。 一种这样的情况是递归方法调用。

消除尾调用(TCE)似乎有必要将环境推送到堆栈上,以便在被调用方法返回后继续进行计算。 但不总是。 如果对方法的调用是调用方法中的最后一件事,则返回时没有任何恢复操作,因此可以直接与当前方法的调用者而不是当前方法本身一起恢复。 在最后一个位置发生的方法调用(即返回之前的最后一件事)称为tail call 。 避免在尾部调用后将环境压入堆栈以恢复方法处理是一种称为尾部调用消除(TCE)的优化技术。 不幸的是,Java没有实现TCE。

消除尾声有时被称为尾声优化(TCO)。 TCE通常是一种优化,我们可能会没有它。 但是,当涉及到递归函数调用时,TCE不再是一种优化。 这是一项强制性功能。 因此,在处理递归时,TCE比TCO更好。

尾递归方法和功能

大多数功能语言都实现了TCE。 但是,TCE不足以使每个递归调用成为可能。 要成为TCE的候选者,递归调用必须是方法必须要做的最后一件事。 考虑以下计算列表元素总和的方法:

static Integer sum(List<Integer> list) {return list.isEmpty()? 0: head(list) + sum(tail(list));}

此方法使用head()tail()方法。 请注意,递归调用sum方法并不是该方法要做的最后一件事。 该方法的最后四件事是:

  • 调用head方法
  • 调用tail方法
  • 调用sum方法
  • head的结果和sum的结果sum

即使我们拥有TCE,我们也无法在10,000个元素的列表中使用此方法,因为递归被调用方不在尾部位置。 但是,可以重写此方法以便将求和的调用放在尾部位置:

static Integer sum_(List<Integer> list) {return sumTail(list, 0);
}static Integer sumTail(List<Integer> list, int acc) {return list.isEmpty()? acc: sumTail(tail(list), acc + head(list));
}

现在, sumTail方法是尾递归的,可以通过TCE进行优化。

抽象递归

到目前为止,一切都很好,但是由于Java不实现TCE,为什么还要烦恼所有这些呢? 好吧,Java没有实现它,但是我们可以不用它。 我们需要做的是:

  • 表示未评估的方法调用
  • 将它们存储在类似堆栈的结构中,直到遇到终端条件
  • 以LIFO顺序评估呼叫

递归方法的大多数示例都使用阶乘函数作为示例。 其他使用斐波那契数列示例。 要开始研究,我们将使用更简单的递归加法。

递归和核心递归函数都是函数,其中f(n)f(n‑1)f(n‑2)f(n‑3) ,依此类推,直到遇到终止条件(通常为f(0)f(1) 请记住,在传统编程中,通常意味着合成评估结果。 这意味着组成函数f(a)g(a)包括对g(a)求值,然后将结果用作f的输入。 不必那样做。 您可以开发一个compose方法来组成函数,而higherCompose函数来完成相同的事情。 此方法或此函数均不会评估所组成的函数。 它们只会产生另一个功能,以后可以应用。

递归和核心递归相似,但有所不同。 我们创建函数调用列表,而不是函数列表。 使用corecursion,每个步骤都是最终步骤,因此可以对其进行评估以便获得结果并将其用作下一步的输入。 通过递归,我们从另一端开始。 因此,我们必须将未评估的调用放入列表中,直到找到终止条件为止,从中我们可以按相反的顺序处理列表。 换句话说,我们堆叠步骤(不评估它们)直到找到最后一个步骤,然后我们以相反的顺序处理堆叠(后进先出),评估每个步骤并将结果用作下一个输入(实际上是前一个)。

我们遇到的问题是Java为此使用了线程堆栈,并且其容量非常有限。 通常,堆栈将在6,000到7,000个步骤之间溢出。

我们要做的是创建一个返回未评估步骤的函数或方法。 为了表示计算中的步骤,我们将使用一个名为TailCall的抽象类(因为我们希望表示对出现在尾部位置的方法的调用)。

这个TailCall抽象类将有两个子类:一个代表中间调用,当暂停一个步骤的处理以调用用于评估下一步的新方法时。 这将由名为Suspend的类表示。 将使用Supplier<TailCall>>实例化它,它表示下一个递归调用。 这样,我们将把每个尾部调用与下一个尾部链接起来,而不是将所有的TailCalls放在列表中。 这种方法的好处是,这样的链表实际上是一个堆栈,可提供恒定的时间插入以及对最后插入的元素的恒定时间访问,这对于LIFO结构是最佳的。

第二个实现将代表最后一个调用,假定将返回结果。 因此,我们将其称为Return 。 它不会保存到下一个TailCall的链接,因为接下来没有任何内容,但是它将保存结果。 这是我们得到的:

import java.util.function.Supplier;public abstract class TailCall<T> {public static class Return<T> extends TailCall<T> {private final T t;public Return(T t) {this.t = t;}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;private Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}}
}

要处理这些类,我们将需要一些方法:一个返回结果,一个返回下一个调用,以及一个帮助程序方法,确定TailCallSuspend还是Return 。 我们可以避免使用最后一种方法,但是我们必须使用instanceof来完成这项工作,这很丑陋。 这三种方法将是:

public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();

resume方法在Return中将没有实现,只会抛出运行时异常。 我们API的用户不应处于调用此方法的情况,因此,如果最终调用该方法,则将是一个错误,并且我们将停止该应用程序。 在Suspend类中,它将返回下一个TailCall

eval方法将返回存储在Return类中的结果。 在我们的第一个版本中,如果在Suspend类上调用它将抛出运行时异常。

isSuspend方法将在Suspend返回true ,在ReturnReturn false 。 清单1显示了第一个版本。

清单1: TailCall抽象类及其两个子类

import java.util.function.Supplier;public abstract class TailCall<T> {public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();public static class Return<T> extends TailCall<T> {private final T t;public Return(T t) {this.t = t;}@Overridepublic T eval() {return t;}@Overridepublic boolean isSuspend() {return false;}@Overridepublic TailCall<T> resume() {throw new IllegalStateException("Return has no resume");}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;public Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}@Overridepublic T eval() {throw new IllegalStateException("Suspend has no value");}@Overridepublic boolean isSuspend() {return true;}@Overridepublic TailCall<T> resume() {return resume.get();}}
}

现在,要使我们的递归方法可以在任意数量的步骤中工作(在可用内存大小的限制之内!),我们几乎不需要做任何更改。 从我们的原始方法开始:

static int add(int x, int y) {return y == 0? x: add(++x, --y)  ;
}

我们只需要进行清单2中所示的修改即可。

清单2:修改后的递归方法

static TailCall<Integer> add(int x, int y) {  // #1return y == 0? new TailCall.Return<>(x)   // #2: new TailCall.Suspend<>(() -> add(x + 1, y – 1));  // #3
}
  • #1方法现在返回一个TailCall
  • #2在终端条件下,返回Return
  • #3在非终止条件下,返回挂起

现在,我们的方法返回TailCall<Integer>而不是int(#1)。 如果已经达到终止条件,则此返回值可以是Return<Integer> (#2),如果尚未达到,则可以是Suspend<Integer> (#3)。 Return用计算的结果实例化(因为y为0,所以x是x),而Suspend则用Supplier<TailCall<Integer>>实例化,后者是根据执行顺序进行下一步的计算,或者就调用顺序而言,前一个。 重要的是要了解,Return对应于方法调用的最后一步,但对应于评估的第一步。 另请注意,我们对评估进行了少许更改,用x + 1y – 1替换了++x--y 。 这是必要的,因为我们使用的是闭包,仅当对变量的闭包实际上是最终的时才起作用。 这是骗人的,但没有那么多。 我们可以使用原始运算符创建并调用dec和inc这两个方法。

此方法返回的是一系列TailCall实例,所有实例均为Suspend实例,最后一个实例为Return。

到目前为止,还算不错,但是显然,这种方法并不能代替原始方法。 没什么大不了的! 原始方法用于:

System.out.println(add(x, y))

我们可以这样使用新方法:

TailCall<Integer> tailCall = add(3, 100000000);while(tailCall .isSuspend()) {tailCall = tailCall.resume();}System.out.println(tailCall.eval());

看起来不是很好吗? 好吧,如果您感到沮丧,我可以理解。 您认为我们将以透明的方式使用新方法代替旧方法。 我们似乎离这很远。 但是,我们可以毫不费力地使事情变得更好。

直接替换堆栈基础递归方法

在上一节的开头,我们说过,递归API的用户将没有机会通过在Return上调用resume或在Suspend上调用eval来弄乱TailCall实例。 通过将评估代码放在Suspend类的eval方法中,可以轻松实现:

public static class Suspend<T> extends TailCall<T> {...@Overridepublic T eval() {TailCall<T> tailRec = this;while(tailRec.isSuspend()) {tailRec = tailRec.resume();}return tailRec.eval();}

现在,我们可以以更简单,更安全的方式获得递归调用的结果:

add(3, 100000000).eval()

但这还不是我们想要的。 我们想要摆脱对eval方法的调用。 这可以通过一个辅助方法来完成:

import static com.fpinjava.excerpt.TailCall.ret;
import static com.fpinjava.excerpt.TailCall.sus;. . .public static int add(int x, int y) {return addRec(x, y).eval();
}private static TailCall<Integer> addRec(int x, int y) {return y == 0? ret(x): sus(() -> addRec(x + 1, y - 1));
}

现在,我们可以完全像原始方法一样调用add方法。 请注意,通过提供静态工厂方法实例化Return和Suspend,我们使递归API更易于使用:

public static <T> Return<T> ret(T t) {return new Return<>(t);
}public static <T> Suspend<T> sus(Supplier<TailCall<T>> s) {return new Suspend<>(s);
}

清单3显示了完整的TailCall类。 我们添加了一个私有的no arg构造函数,以防止被其他类扩展。

清单3:完整的TailCall

package com.fpinjava.excerpt;import java.util.function.Supplier;public abstract class TailCall<T> {public abstract TailCall<T> resume();public abstract T eval();public abstract boolean isSuspend();private TailCall() {}public static class Return<T> extends TailCall<T> {private final T t;private Return(T t) {this.t = t;}@Overridepublic T eval() {return t;}@Overridepublic boolean isSuspend() {return false;}@Overridepublic TailCall<T> resume() {throw new IllegalStateException("Return has no resume");}}public static class Suspend<T> extends TailCall<T> {private final Supplier<TailCall<T>> resume;private Suspend(Supplier<TailCall<T>> resume) {this.resume = resume;}@Overridepublic T eval() {TailCall<T> tailRec = this;while(tailRec.isSuspend()) {tailRec = tailRec.resume();}return tailRec.eval();}@Overridepublic boolean isSuspend() {return true;}@Overridepublic TailCall<T> resume() {return resume.get();}}public static <T> Return<T> ret(T t) {return new Return<>(t);}public static <T> Suspend<T> sus(Supplier<TailCall<T>> s) {return new Suspend<>(s);}
}

既然您有了堆栈安全的尾部递归方法,那么可以对函数执行相同的操作吗? 在我的《 Java中的函数式编程》一书中,我谈到了如何做到这一点。

翻译自: https://www.javacodegeeks.com/2015/10/stack-safe-recursion-in-java.html

java 递归 堆栈

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

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

相关文章

cshtml中引用css_css基础必备-使用样式,前端小白一看就会

在HTML文档中包含CSSCSS可以作为单独的文档引用&#xff0c;也可以嵌入到HTML文档中。在HTML文档中包含CSS有三种方法&#xff1a;内联样式 - 使用元素起始标记的style属性指定样式嵌入样式 - 在文档的head部分使用style标记指定样式外部样式 - 在文档的head部分使用link标记引…

嵌入式软件分层框架设计,举个例子

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删前言为了能够使得产品得到更好的开发速度与以后更好的迭代和移植&#xff0c;框架分层是很有必要的。但如对于中小型项目严格遵循这些原则&#…

腾讯大举退出美团!

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删11月16日&#xff0c;腾讯发布第三季度财报&#xff0c;同时表示将“分红式减持”美团。腾讯分派90.9%美团持股 腾讯系中概股美股盘前多数下跌腾…

matchers依赖_Hamcrest Matchers教程

matchers依赖本文是我们名为“ 用Mockito测试 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入了解Mockito的魔力。 您将了解有关“模拟”&#xff0c;“间谍”和“部分模拟”的信息&#xff0c;以及它们相应的存根行为。 您还将看到使用测试双打和对象匹配器进行验证…

谷歌开源替代 C++ 的编程语言:Carbon

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删谷歌工程师 Chandler Carruth 近日在多伦多举办的 CppNorth 大会上宣布①&#xff0c;正式开源谷歌内部打造的编程语言&#xff1a;Carbon&#…

C语言灵魂拷问:++i为何比i++执行效率高!有何区别?

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删背景相信很多人遇到过这样的问题&#xff1a;printf("%d,%d",i,i);也纠结过这个问题&#xff0c;到底答案是什么。确没有一个参考的资…

指标实现层级_企业如何构建核心指标系统,实现业务运营效率提升90%?

本文为帆软数据生产力大赛获奖案例&#xff0c;未经授权禁止转载。01企业简介西安怡康医药连锁有限责任公司成立于2001年&#xff0c;总部设在西安市大庆路副24号,是一家由零售连锁药店发展起来的大型医药连锁企业&#xff0c;除药品零售外&#xff0c;同时兼营药品批发与器械批…

学生时代,你有遇到过像我这样理解C语言的吗?

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删今天我讲一下我个人对C语言的认识以及自己的理解&#xff0c;若有错误&#xff0c;还望指出&#xff0c;不甚感激。首先是C语言整体的脉络&#…

scrapyd部署_第八章 第一节 scrapyd和scrapy-client

如果觉得文章对您产生帮助的话, 欢迎关注Python开发之路(微信公众号: python-developer), 及时获取更多教程假设有我们做了一个项目是抓取很多网站(每个网站对应一个爬虫), 需要分别部署到不同的服务器上(单台扛不住), scrapy官方也提供了一种部署工具scrapyd。这个工具是用来将…

C++ 首超 Java,与 Python、C语言共角逐年度最佳编程语言奖!

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删最新的 TIOBE 12 月编程语言已发布&#xff0c;先来预测一波今年的年度编程语言大奖究竟会花落谁家吧&#xff1f;C 首超 Java和上个月相比&…

乔安监控云存储能存多长时间_干货 | 监控磁盘阵列知识介绍,不了解还不来看看?...

一、磁盘阵列的概念要定义磁盘阵列的概念&#xff0c;是一个简单的工作&#xff0c;因为这个概念已经形成了共识——磁盘阵列(DiskArray)是由一个硬盘控制器来控制多个硬盘的相互连接&#xff0c;使多个硬盘的读写同步&#xff0c;减少错误&#xff0c;增加效率和可靠度的技术。…

我要是在学习 C 语言之前知道这些就好了!

点击蓝字关注我们因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源于网络&#xff0c;侵删对于我来说&#xff0c;学习 C 语言好难啊。这门语言本身的基础知识并不是很难&#xff0c;但是“用 C 语言编程”需要用到各种知识&#xff0c;…

opencv yuv保存本地_OpenCV-dlib-python3实现人脸戴墨镜和含Y的抖音效果

1 说明&#xff1a;1.1 吸烟有害健康&#xff01;&#xff01;纯属娱乐和学习python的相关知识。1.2 虽然是娱乐&#xff0c;但是opencv、dlib和python在人工智能、人脸识别、自动化等有很大作用&#xff0c;目前已经或者未来会有更多的应用&#xff0c;作为一名普通人&#xf…

为什么永远不会有语言取代 C/C++?

关注星标&#xff0c;每天学习C语言新技能因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源&#xff1a;网络数以百计的 C/C 替代品将会出现&#xff0c;但 C/C 将永远与我们同在&#xff01;每个 CPU 都带有一种称为 ISA&#xff08;指…

qt 表格中插入一行_在EXCEL表格中,快速插入多行、多列的技巧

在使用Excel过程中&#xff0c;我们会遇到需要插入相同格式的多行或多列&#xff0c;如果一行行或一列列的插入&#xff0c;对于插入的数量较少的情况还是适用的。可是如果需要插入上百的行或列&#xff0c;使用此方法就比较费时费力啦。分享几个小技巧实现快速插入多行或多列.…

amber 口译_口译员设计模式示例

amber 口译本文是我们名为“ Java设计模式 ”的学院课程的一部分。 在本课程中&#xff0c;您将深入研究大量的设计模式&#xff0c;并了解如何在Java中实现和利用它们。 您将了解模式如此重要的原因&#xff0c;并了解何时以及如何应用模式中的每一个。 在这里查看 &#xff…

别再自己瞎写工具类了,SpringBoot内置工具类应有尽有,建议收藏!!

关注星标&#xff0c;每天学习C语言新技能因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源&#xff1a;网络断言断言是一个逻辑判断&#xff0c;用于检查不应该发生的情况Assert 关键字在 JDK1.4 中引入&#xff0c;可通过 JVM 参数-en…

ad转换器工作原理_AD转换中参考电压的作用

AD转换AD转换就是模数转换。顾名思义&#xff0c;就是把模拟信号转换成数字信号。主要包括积分型、逐次逼近型、并行比较型/串并行型、Σ-Δ调制型、电容阵列逐次比较型及压频变换型。A/D转换器是用来通过一定的电路将模拟量转变为数字量。模拟量可以是电压、电流等电信号&…

面试大全 | C语言高级部分总结

关注星标&#xff0c;每天学习C语言新技能因公众号更改推送规则&#xff0c;请点“在看”并加“星标”第一时间获取精彩技术分享来源&#xff1a;网络一、内存大话题1.0、内存就是程序的立足之地&#xff0c;体现内存重要性。1.1、内存理解&#xff1a;内存物理看是有很多个Ban…

ideal pom文件安装到maven库中_java学习之web基础(14)Maven基础学习

maven介绍Maven 是一个项目管理工具&#xff0c;它包含了一个项目对象模型 (POM&#xff1a; Project Object Model)&#xff0c;一组标准集合&#xff0c;一个项目生命周期(Project Lifecycle)&#xff0c;一个依赖管理系统(Dependency Management System)&#xff0c;和用来运…