我正在尝试编写一个优化的fibonacci作为能够计算fib(300)和fib(8000)的赋值。这是我到目前为止(map是一个HashMap)。
public static BigInteger fibMemo(int n) {
if (n <= 1){
return BigInteger.valueOf(n);
}
if (!map.containsKey(n)){
map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
}
return map.get(n);
}
当我打电话
System.out.println("300: " + FibonacciMemo.fibMemo(300));
自己,它运作得很好。 此外,
System.out.println("8000: " + FibonacciMemo.fibMemo(8000));
如果我注释掉之前对fib(300)的调用,工作正常。但是,如果我保持两个调用,我会在递归fibMemo上获得堆栈溢出。这对我来说似乎很奇怪。有人可以澄清一下情况吗?提前谢谢。
以下是代码:
import java.util.HashMap; // Import Java's HashMap so we can use it
import java.math.BigInteger;
public class FibonacciMemo {
private static HashMap<Integer, BigInteger> map = new HashMap<Integer, BigInteger>();
/**
* The classic recursive implementation with no memoization. Don't care
* about graceful error catching, we're only interested in performance.
*
* @param n
* @return The nth fibonacci number
*/
public static int fibNoMemo(int n) {
if (n <= 1) {
return n;
}
return fibNoMemo(n - 2) + fibNoMemo(n - 1);
}
/**
* Your optimized recursive implementation with memoization.
* You may assume that n is non-negative.
*
* @param n
* @return The nth fibonacci number
*/
public static BigInteger fibMemo(int n) {
// YOUR CODE HERE
if (n <= 1){
return BigInteger.valueOf(n);
}
if (!map.containsKey(n)){
map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
}
return map.get(n);
}
public static void main(String[] args) {
// Optional testing here
String m = "Fibonacci's real name was Leonardo Pisano Bigollo.";
m += "\n" + "He was the son of a wealthy merchant.\n";
System.out.println(m);
System.out.println("300: " + FibonacciMemo.fibMemo(300));
System.out.println("8000: " + FibonacciMemo.fibMemo(8000));
// 46th Fibonacci = 1,836,311,903
// 47th Fibonacci = 2,971,215,073
}
}
答案 0 :(得分:2)
您的代码存在两个问题。显而易见的是堆栈消耗。 memoization确实将时间复杂度从指数降低到线性,但是,该方法仍然具有线性堆栈消耗 - 对于输入值8000,它分配8000个堆栈帧。
正如docs中所述,每个线程的默认堆栈大小 320kB ,大约需要1000到2000帧,这还不够。您可以使用-Xss
JVM开关增加堆栈大小,但这仍然不是防弹。您应该使用迭代实现。
第二个问题是你的静态缓存永远不会被清除,这基本上会导致内存泄漏。您可以将递归方法包装在另一个递归方法中,该方法在递归终止后清除散列映射,但这会丢弃一些性能,因为一次调用的结果不能在以下方法中重用。
更高性能的解决方案是使用不需要手动清理的适当缓存实现,但是自己处理大小限制和垃圾收集。 Guava提供了此类实施。
答案 1 :(得分:1)
看起来你的递归算法对于Java的默认堆栈大小来说太多了。堆栈内存的优化与硬件中的堆不同,无论如何都不应该使用这种递归算法。有些语言可以优化尾递归。至少在这种情况下,Java似乎不会总是优化您的代码。
因此,最好的解决方案就是重写代码以改为使用循环。
private final static List<BigInteger> fibs = new ArrayList<>();
static{ fibs.add( BigInteger.ZERO ); fibs.add( BigInteger.ONE ); }
public static BigInteger lFib( int n ) {
if( n < 0 ) throw new IllegalArgumentException();
if( n >= fibs.size() ) {
for( int i = fibs.size(); i <= n; i++ )
fibs.add( fibs.get(i-2).add( fibs.get(i-1) ) );
}
return fibs.get(n);
}
非常轻微的测试。
答案 2 :(得分:0)
问题是线程堆栈的大小,它可能会耗尽大量的递归调用。解决方案是提供足够的堆栈大小。您可以尝试使用vm arg -Xss
运行该应用。我尝试使用-Xss2m
并且效果很好。
答案 3 :(得分:0)
更改您的代码
map.put(n, fibMemo(n-1).add(fibMemo(n-2)));
到
map.put(n, fibMemo(n-2).add(fibMemo(n-1)));
工作正常。
前者的呼叫序列是
fibMemo(10) nested level = 0
fibMemo(9) nested level = 1
fibMemo(8) nested level = 2
fibMemo(7) nested level = 3
fibMemo(6) nested level = 4
fibMemo(5) nested level = 5
fibMemo(4) nested level = 6
fibMemo(3) nested level = 7
fibMemo(2) nested level = 8
fibMemo(1) nested level = 9
fibMemo(0) nested level = 9
fibMemo(1) nested level = 8
fibMemo(2) nested level = 7
fibMemo(3) nested level = 6
fibMemo(4) nested level = 5
fibMemo(5) nested level = 4
fibMemo(6) nested level = 3
fibMemo(7) nested level = 2
fibMemo(8) nested level = 1
后者的呼叫序列是
fibMemo(10) nested level = 0
fibMemo(8) nested level = 1
fibMemo(6) nested level = 2
fibMemo(4) nested level = 3
fibMemo(2) nested level = 4
fibMemo(0) nested level = 5
fibMemo(1) nested level = 5
fibMemo(3) nested level = 4
fibMemo(1) nested level = 5
fibMemo(2) nested level = 5
fibMemo(5) nested level = 3
fibMemo(3) nested level = 4
fibMemo(4) nested level = 4
fibMemo(7) nested level = 2
fibMemo(5) nested level = 3
fibMemo(6) nested level = 3
fibMemo(9) nested level = 1
fibMemo(7) nested level = 2
fibMemo(8) nested level = 2
后者对堆栈的消耗较少。