【问题标题】:Achieving Stackless recursion in Java 8在 Java 8 中实现无堆栈递归
【发布时间】:2015-12-17 14:07:54
【问题描述】:

如何在 Java 中实现无堆栈递归?

似乎出现最多的词是“蹦床”,我不知道那是什么意思。

有人可以详细说明解释如何在 Java 中实现无堆栈递归吗?另外,什么是“蹦床”?

如果您不能提供其中任何一个,请您指出正确的方向(即,一本可以阅读的书或一些教授所有这些概念的教程)?

【问题讨论】:

标签: java recursion stack-overflow tail-call-optimization


【解决方案1】:

蹦床是一种将基于堆栈的递归转换为等效循环的模式。由于循环不添加堆栈帧,因此可以将其视为一种无堆栈递归。

这是一个我觉得很有帮助的图表:

来自bartdesmet.net

你可以把蹦床想象成一个接受起始值的过程;迭代该值;然后以最终值退出。


考虑这个基于堆栈的递归:

public static int factorial(final int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

对于每个递归调用,都会推送一个新帧。这是因为如果没有新帧的结果,前一帧就无法评估。当堆栈变得太深并且内存不足时,这将成为一个问题。

幸运的是,我们可以将这个函数表示为一个循环:

public static int factorial2(int n) {
    int i = 1; 
    while (n > 1) {
        i = i * n;
        n--;
    }
    return i;
}

这里发生了什么?我们已经采取了递归步骤,并使其成为循环内的迭代。我们循环直到完成所有递归步骤,将结果或每次迭代存储在一个变量中。

这更有效,因为将创建更少的帧。我们不是为每个递归调用存储一个帧(n 帧),而是存储当前值和剩余的迭代次数(2 个值)。

这种模式的概括是蹦床。

public class Trampoline<T>
{
    public T getValue() {
        throw new RuntimeException("Not implemented");
    }

    public Optional<Trampoline<T>> nextTrampoline() {
        return Optional.empty();
    }

    public final T compute() {
        Trampoline<T> trampoline = this;

        while (trampoline.nextTrampoline().isPresent()) {
            trampoline = trampoline.nextTrampoline().get();
        }

        return trampoline.getValue();
    }
}

Trampoline 需要两个成员:

  • 当前步骤的值;
  • 下一个要计算的函数,如果我们已经到了最后一步,则什么都不做

任何可以用这种方式描述的计算都可以被“蹦床”。

阶乘是什么样的?

public final class Factorial
{
    public static Trampoline<Integer> createTrampoline(final int n, final int sum)
    {
        if (n == 1) {
            return new Trampoline<Integer>() {
                public Integer getValue() { return sum; }
            };
        }
        
        return new Trampoline<Integer>() {
            public Optional<Trampoline<Integer>> nextTrampoline() {
                return Optional.of(createTrampoline(n - 1, sum * n));
            }
        };
    }
}

然后调用:

Factorial.createTrampoline(4, 1).compute()

备注

  • Boxing 会降低 Java 的效率。
  • 此代码是在 SO 上编写的;它尚未经过测试甚至编译

进一步阅读

【讨论】:

  • 以上蹦床调用为 factorial.execute(4) 给出 -1198722684
【解决方案2】:

来自Wikipedia

蹦床是一个循环调用 thunk-returning 功能

在自己对蹦床进行一些研究时,我偶然发现了上面的那条线。因此,我对thunk 在 Java 中的工作方式和外观进行了一些试验。我得到了值得分享的令人满意的结果。这是对先前答案的相当大的升级,因为 thunk 看起来与递归函数非常相似。这是由于自 java 8 以来添加的functional interfaces


// Recursive
private int factorial(final int number) {
    return (number == 1)
        ? 1 
        : number * factorial(number -1);
}

将执行阶乘(上)的递归方式与下面的 thunk 返回版本进行比较。注意到相似之处了吗?

// The thunk returning version
private Pair<Supplier, Long> factorial(long number, long sum){
    return (number == 1)
        ? new Pair<>(null, sum)
        : new Pair<>(() -> factorial(number -1, sum * number), null);
}

我在这里使用了Java的Pair,你可以认为它是一个穷人的Tupel。在您自己的项目中,您可以在 Supplier 函数上使用多态性,如果您愿意的话。两者都应该工作。


为了调用 thunk 返回版本,我使用下面的 while 循环。

private long trampoline(long number) {
    Pair<Supplier, Long> supplierOrResult = new Pair<>(()-> factorial(number, 1), null);

    while (supplierOrResult.getValue() == null) {
        supplierOrResult = (Pair<Supplier, Long>)supplierOrResult.getKey().get();
    }

    return supplierOrResult.getValue();
}

当您回顾维基百科的引用时,您会发现这里的 while 循环就是蹦床。我们有一个while loop,它调用一个function,它返回一个thunk

【讨论】:

    猜你喜欢
    • 2020-08-27
    • 2014-03-14
    • 2018-03-26
    • 2021-06-24
    • 1970-01-01
    • 1970-01-01
    • 2019-11-11
    • 1970-01-01
    • 2017-09-14
    相关资源
    最近更新 更多