尾递归函数仍然在Java中使堆栈崩溃

问题描述

我正在尝试实现尾递归阶乘计算器,但仍然出现堆栈溢出。谁能帮我找出原因?

  • 我已经阅读到Java 8支持尾部调用优化,但是我认为我不能正确实现它。
  • 我已阅读到可以使用lambda表达式。我不确定我是否完全理解这个概念,但是我仍在阅读。
  • 我只是在寻找有关如何使用真正的尾部调用优化,lambda表达式的建议,或者可以。

代码:

package factorielRecursiveTerminale;

import java.math.BigInteger;
import java.util.Scanner; 

public class factorielRecursiveTerminale {  
    
    public static BigInteger factoriel(BigInteger n,BigInteger m) {
        if (n.compareTo(BigInteger.ZERO) < 1) return m;             
        return factoriel(n.subtract(BigInteger.ONE),n.multiply(m));
    }                                                               
                                                                
    public static BigInteger fact(int n) {  //convertir l'entree en BigInteger et lancer la recursion
        if(n < 0) {
            return BigInteger.valueOf(-1);
        }
        BigInteger b = BigInteger.valueOf(n);
        return factoriel(b,BigInteger.ONE);
    }

    public static void runBigFact() {                       //gestion des erreurs + boucle d'entree de valeurs. 
        String valeurRecu = "";
        int valeur;
        BigInteger resultat;
        System.out.println("Calcul Factoriel\n");
        while(!valeurRecu.contentEquals("q")){
            System.out.println("Entrer la valeur a calculer (q - quitter) : ");
            Scanner entree = new Scanner(System.in);
            valeurRecu = entree.nextLine();
            if (valeurRecu.contentEquals("q")) entree.close();
            else {
                try {
                    valeur = Integer.parseInt(valeurRecu);
                }catch (NumberFormatException e){  
                    System.out.println("Pas un entier. Essayer encore.\n");
                    continue;
                  } 
                try {
                    resultat = fact(valeur);
                    if(resultat.compareTo(BigInteger.valueOf(-1)) == 0) {
                        System.out.println("Valeur negative. Essayer encore.\n");
                    }
                    else System.out.println("Factoriel " + valeur + " -> " + fact(valeur) + "\n");
                } catch(StackOverflowError e) {
                    System.out.println("Depassement de la pile. Essayer un entier plus petit.\n");
                    continue;
            }
        }
        }
        System.out.println("Au revoir! :)\n");
    }
    
    public static void main(String[] args) {
        runBigFact();
    }
}

解决方法

我已经了解到JAVA 8支持尾部调用优化,但是我认为我不能正确实现它。

那么您读错了。或者,您已经阅读了正确的声明,但是没有正确解释它。

Java语言不支持尾调用递归。它从来没有。可能永远不会*。

但是,Java(VM)具有一些功能,这些功能使其他非Java语言(它们仍可编译成类文件在Java运行时上运行)更容易支持TCO。大概就是您所读到的。

我只是在寻找有关如何使用真正的尾部调用优化,lambda表达式的建议,或者可以。

在scala或类似语言中将其写入。

严重的是,java如何没有TCO?

TCO非常昂贵:Java遵循这样的规则:发生错误时,您将获得堆栈跟踪,并且堆栈跟踪是一个定义明确的概念,至关重要的是,每个逻辑调用都跟踪一个堆栈帧。如果存在TCO,则此无法继续。当然,有一些选择:堆栈上的每个单独的帧都可以获取一个“计数器”,以便堆栈跟踪在保持正确表示“并且此调用序列已重复8190581次”的情况下仍保持较小的内存占用。这也是lang规范中有关其工作方式,何时起作用和不起作用以及它们的全部含义的大量文本,而且规范中的任何其他页面永远都是维护负担-并非“它”将TCO添加到Java绝对是更好的选择,所以当我们处理它,灌篮以及具有该功能的任何Pull Requests时,都将立即集成。

此外,TCO作为一种模型是一种做事的方法,但这不是唯一的方法。对于任何可以作为TCO递归应用程序编写的东西,通常都很难将其重构为基于循环的非递归算法。例如,与基于收益的异步操作相反,您当然可以在其中进行重写(嘿,这都是图灵机),但是重写将很困难,并且所产生的代码也很难理解。我不想谈谈yield / async样式编码的价值(或缺乏价值),只是要指出TCO不具有'ah饰面,但是 if TCO是好主意,那么只有TCO可以做到。

我没有临时可用的链接,但是那些对Java的未来有很大影响的人(例如Brian Goetz,Mark Reinhold等)已经表达了这种观点。真正致力于尝试将其添加到Java中,建议您在网络上搜索这些语句,然后尝试专门设计一些参数来解决它们所陈述的问题。因为如果您不能说服那些人,那就永远不会发生。

那我在Java中做什么?

不使用递归;改用whilefor

更新:那篇博客文章呢?

在注释中,您已链接到this blog entry。那不是TCO。

这是使用lambdas编写的框架,可让您或多或少地模拟TCO,但这不是TCO。该博客描述了一个小框架-因此,您需要粘贴它们的所有内容:特别是TailCall接口。

该代码如下:

  • 您的“递归”方法根本不是递归的,它总是在不调用自身的情况下快速返回。
  • 它返回一个可以自称为lambda的值。但是,正如我们刚刚介绍的那样,调用自己会快速返回而无需递归,并且它会返回一个函数。
  • 框架将执行您的函数,该函数通常会产生一个函数(或实际结果)。循环(因此没有递归),重复执行以下过程:“调用该函数。如果返回一个函数,则循环。如果返回一个结果,好吧,那就是我们想要的结果,所以只需返回那个。”

它描述了TCO试图完成的工作(反复使用不同的参数反复调用相同的功能,直到达到硬编码的边缘情况,然后再撤回),但没有使用TCO来完成。

因此,该博客文章说:“看,Java的TCO!”具有误导性。

这就像我在说:“看,隧道墙上的画笔!”并描述了如何使用喷涂涂料以看起来就像用手刷一样的方式对隧道壁进行喷涂。很好,但是将其称为“粉刷墙壁”是一种误导。充其量您可以说:“想要在隧道中制作画笔样式的艺术品?嗯,您不能,我也不能解决这个问题,但是我可以告诉您如何获得类似的结果!”。


*)永远都不要说永不止息,但我的意思是:目前还没有任何计划,而Java平台的未来计划要走很多年,而且是公开的。对于“ Java(该语言)在4年内没有尾调用递归”,我会采用1到40的赔率,并且仍然采用这种赌注。

,

您可能会发现这很有用。与以前的尝试相比,我可以得到一些改进,但是在这种情况下,导致SO的不是BigInteger对象的大小。在我的机器上,这两种方法都导致n的14000和15000之间的堆栈溢出。 simpleLong只是递减Long的一种基本递归方法,它仍然在15000处爆炸。在14000处都成功。

public static void main(String[] args) {
    count = 0;
    long n = 14000;
    simpleLong(n);
    factoriel(BigInteger.valueOf(n));
    
}
    
static BigInteger factoriel(BigInteger n) {
    if (n.compareTo(BigInteger.TWO) == 1) {
        return factoriel(n.subtract(BigInteger.ONE)).multiply(n);
    }
    return n;
}
    
static long simpleLong(long n) {
    if (n > 1) {
        simpleLong(n-1);
    }
    return n;
}

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...