这里有三种计算阶乘N的对数的方法:lgN! 第一种方法使用递归并将每个递归值存储在数组中 第二种方法使用不带数组的递归 第三种方法使用FOR循环。
经过测试,第三种方法的性能最好,第二种方法的第一种方法的性能很差。
我的问题是为什么第一种方法比第二种方法差 尽管第三种方法具有最佳性能,但它的价值与其他方法不同,但是有什么错误吗?
/**
* method 1
* calculate lgN! with recursion and keep every recursive value
* in an array. It has bad performance, once n reached about 6000, it
* will throw stackoverflow exception
* @param a
* @param n
* @return
*/
private static double lgNFact1(double[] a ,int n){
if(n==1)
a[n-1]=0;
else
a[n-1]=Math.log(n) + lgNFact1(a,n-1);
return a[n-1];
}
/**
* Method 2 have a little better performance, in my case n can reach 10000!,than throws stackoverflow exception
* @param n
* @return
*/
private static double lgNFact2(int n) {
if(n==1)
return 0;
return Math.log(n)+lgNFact2(n-1);
}
/**
* Method 3, based on logarithm formula, we can use for loop to get lgN!
* log5!=log5+log4+log3+log2+log1
* @param n
* @return
*/
private static double lgNFact3(int n) {
double sum = 0;
for(int i=1;i<=n;i++) {
sum+=Math.log(n);
}
return sum;
}
下面是测试,当n等于10时,我们可以看到第三个与上面两种情况不同
case1 n = 10,结果:15.104412573075518
case2 n = 10,结果:15.104412573075518
case3 n = 10,结果:23.025850929940464
答案 0 :(得分:0)
使用递归解决方案时,对函数的每次调用都会为该函数设置内存位置,并在执行计算之前连接有关调用者的元数据等。 for循环仅计算值并完成。第一种方法除了要处理数字以外,还必须处理复制数组,因此需要花费更长的时间。
答案 1 :(得分:0)
总结:方法1正在堆栈上复制一个数组,这需要时间和内存,因此其上限为6000。方法2是递归的,因此也可以限制堆栈的增长量。它必须花费时间为每个递归调用分配内存。方法3不使用递归调用,因此不必膨胀堆栈,也不必花费时间推送堆栈帧。
答案 2 :(得分:0)
哪种方法最有效或方法2的尾调用变式取决于语言。就目前而言,方法2不使用尾调用递归。一个简单的更改以使其使用尾调用:
/**
* Tail call method 2 may or may not throw a stackoverflow exception, depending
* on whether the language supports tail call optimization.
*/
private static double tailLgNFact2 (int n, double sum=0.0)
{
if (n <= 1) return sum;
return tailLgNFact2(n-1, Math.log(n)+sum);
}
基于堆栈溢出时的其他问题,Java编译器显然不支持尾部调用优化,但是某些Java运行时显然支持。使用确实支持尾部调用优化的功能编程语言,尾部调用版本可能比循环版本具有更好的性能。