java 递归 堆栈_Java中的堆栈安全递归
java 递归 堆栈
在本文中,摘自《 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;
}
注意,直接使用参数x
和y
没问题,因为在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;}}
}
要处理这些类,我们将需要一些方法:一个返回结果,一个返回下一个调用,以及一个帮助程序方法,确定TailCall
是Suspend
还是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
,在Return
中Return
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 + 1
和y – 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 递归 堆栈
java 递归 堆栈_Java中的堆栈安全递归相关推荐
- java 队列和堆栈_Java中的堆栈和队列
java 队列和堆栈 我最近一直在研究一些需要堆栈和队列的Java代码. 使用的选择不是立即显而易见的. 有一个Queue接口,但没有明确的具体实现要使用. 还有一个Stack类,但是javadocs ...
- java 递归终止_java中执行程序如何终止递归?
这是一个程序,读取信息网站为以前的格式,它使用递归和executor.It工作正常,我的问题是测试程序是否完成和成功通知.java中执行程序如何终止递归? public class NewClass ...
- java递归和迭代_Java中的迭代与递归
递归 提到迭代,不得不提一个数学表达式: n!=n*(n-1)*(n-2)*...*1 有很多方法来计算阶乘.有肯定数学基础的人都知道n!=n*(n-1)!因而,代码的实现可以直接写成: 代码一 in ...
- java 递归原理_Java中递归原理实例分析
本文实例分析了Java中递归原理.分享给大家供大家参考.具体分析如下: 解释:程序调用自身的编程技巧叫做递归. 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中 ...
- java的位置_Java中数据存放的位置
Java程序运行的时候,数据一般保存到什么地方?下面来详细讲一下. (1)寄存器.这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部.然而,寄存器的数量十分有限,所以寄存器是根据需 ...
- java存储数据_Java中六种数据存储方式
存储数据 1.寄存器(register).这是最快的存储区,因为它位于不同于其他存储区的地方--处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配.你不能直接控制,也不能在程序中 ...
- java mod %区别_Java中 % 与Math.floorMod() 区别详解
%为取余(rem),Math.floorMod()为取模(mod) 取余取模有什么区别呢? 对于整型数a,b来说,取模运算或者取余运算的方法都是: 1.求 整数商: c = a/b; 2.计算模或者余 ...
- java show过时_Java中show() 方法被那个方法代替了? java编程 显示类中信
你说的show是swing里的吧,在老版本中Component这个超类确实有show这个方法,而且这个方法也相当有用,使一个窗口可见,并放到最前面.在jdk5.0中阻止了这个方法,普遍用setVisi ...
- java判断类型_Java中类型判断的几种方式 - 码农小胖哥 - 博客园
1. 前言 在Java这种强类型语言中类型转换.类型判断是经常遇到的.今天就细数一下Java中类型判断的方法方式. 2. instanceof instanceof是Java的一个运算符,用来判断一个 ...
最新文章
- [译] 如何写一篇杀手级的软件工程师简历
- 分享Kali Linux 2017年第29周镜像文件
- 【开源推荐】进阶实战,从一款音乐播放器开始
- hilbert曲线序编码matlab,Hilbert曲线扫描矩阵的生成算法及其MATLAB程序代码
- 《黑客》月刊中文版第一期正式发布,很给力!推荐围观!
- H264中DCT变换,量化,反量化,反DCT变换
- L3-009 长城 (30 分)-PAT 团体程序设计天梯赛 GPLT
- 初了解JS设计模式,学习笔记
- python翻转棋_Python算法做翻转棋子游戏
- ssh框架 mysql 配置文件_SSH框架与配置文件的简单搭建
- Installation error: INSTALL_FAILED_UPDATE_INCOMPATIBLE
- Win11更新或更改时间后闪白屏的解决方法
- python 保存数据单文件_python3.6 单文件爬虫 断点续存 普通版 文件续存方式
- 单位dB(分贝)的含义和好处,dBm(dBmW 分贝毫瓦)的含义
- 九 iOS之 图片剪裁
- gpu服务器厂家_嵌入式主板厂家告诉你选择GPU服务器的5大标准
- 你很聪明,思维也很敏捷,小有才华,为什么依然一事无成?
- Latex入门教程用法笔记(结尾附完整示例)
- 图数据库查询语言Cypher、Gremlin和SPARQL
- 笔记:软件工程常用开源文档模板 + 软件著作权
热门文章
- AT4353-[ARC101D]Robots and Exits【LIS】
- jzoj6274-[NOIP提高组模拟1]梦境【贪心,堆】
- jzoj2137-(GDKOI2004)城市统计【二维前缀和,bfs】
- ssl2342-打击犯罪【并查集】
- ssl初一组周六模拟赛【2018.3.10】
- codeforces 939C Convenient For Everybody 简直羞耻
- 【附答案】Java面试2019常考题目汇总(一)
- Spring Boot Debug调试
- Java 常见的 30 个误区与细节
- ssh(Spring+Spring mvc+hibernate)——applicationContext-servlet.xml