优化Fibonacci
我们要做的第一个优化是消除一个方法调用,如Listing1-3所示。因为这个实现是递归的,在方法中移除一个函数调用会显著的减少总的方法调用次数。比如,computeRecursively(30)产生2692537次调用,而computeRecursivelyWithLoop(30)仅产生1346269次。然而,根据上面定义的标准,100毫秒或者更少的时间,这个方法的性能还是不可以接受,computeRecursivelyWithLoop(30)需要大约270毫秒。
Listing 1-3 优化Fibonacci序列的递归实现
public class Fibonacci {
public static long computeRecursivelyWithLoop(int n) {
if(n>1) {
long result = 1;
do {
result += computeRecursivelyWithLoop(n-2);
n--;
} while(n>1);
return result;
}
return n;
}
}
NOTE:这不是一个真正的尾递归优化。
从递归到迭代
第二个优化,我们将递归实现切换成迭代实现。递归算法对开发者来说声誉很差,特别是在内存不多的嵌入式系统中,因为它们会消耗很多的栈空间,还有像我们刚刚看到的,太多的方法调用。即使性能是可以接受的,一个递归算法可能会引起栈溢出,造成程序crash。因此会尽可能选择迭代算法。Listing 1-4给出了Fibonacci序列的一个教科书式的迭代算法实现。
Listing 1-4 Fibonacci序列的递归实现
public class Fibonacci {
public static long computeIteratively(int n) {
if(n>1) {
long a = 0, b = 1;
do {
long tmp = b;
b += a;
a = tmp;
} while (--n > 1);
return b;
}
return n;
}
}
因为第N个Fibonacci序列的值是前面两个值的简单加和,一个简单的循环就可以做到。和递归算法相比,迭代算法的复杂度同样大幅降低,因为它是线性的。结果,它的性能同样更好,computeIteratively(30)需要少于1毫秒计算完成。因为它的线性特性,你可以使用这样一个算法去计算序列第30个以外的值。比如,computeIteratively(50000)需要大约2毫秒,通过线性投影推算,你可以猜测computeIteratively(500000)将需要20到30毫秒。
尽管这样的性能已经远远超越了可接受的目标(100ms或者更少),对这个算法做一点修改可以得到更快的结果,如Listing 1-5所示。这个新的版本每次循环计算两个值,总的迭代次数减少到一半。由于在最初的迭代算法中迭代次数可能是奇数,对a和b的初始值做相应的修改:当n是奇数的时候,序列从a=0和b=1开始;当n是偶数的时候,序列从a=1和b=1(Fib(2)=1)开始。
Listing 1-5 修改过的Fibonacci序列的迭代实现
public class Fibonacci {
public static long computeIterativelyFaster(int n) {
if (n>1) {
long a, b = 1;
n--;
a = n & 1;
n /= 2;
while (n-- > 0) {
a += b;
b += a;
}
return b;
}
return n;
}
}
结果显示这个修改后的算法运行速度大概是原算法的2倍。
尽管迭代的实现方式已经很快,它有一个主要的问题:可能不会返回正确的结果。问题在于返回的结果通过64位的long保存。64位可以容纳的最大的Fibonacci数值是7,540,113,804,746,346,429,第92个Fibonacci值。尽管当传入值大于92的时候,这个方法会返回而不会发生程序crash,但是因为溢出会返回一个不正确的值:第93个Fibonacci值将会是一个负值!递归实现有同样的限制,不过用户要非常有耐心才会发现。NOTE:Java指定了所有原子类型的大小(除了boolean型):long, 64位;int, 32位;short, 16位。所有的整型数值是有符号数。