上一篇我们使用了迭代替换递归达到优化的目的,但仍存在一个严重的问题:它们不能返回正确的结果。问题归咎于返回的结果值被存储在一个长整形,是64-bit的。但最大的Fibonacci数字能匹配64-bit的值是 7,540,113,804,746,346,429。也就是说92的Fibonacci数。当计算93Fibonacci数时,因为已经溢出,返回的结果将是不正确的。而且发生这样的错误时,非常难以排查。
(友情提示:java特别的指定原始类型(除了boolean):long长整形是64位,int整形是32位,short是16位。)
java提供了一个合适的类去解决这个溢出问题:java.math.BigInteger.
一个BigInteger对象能够指定任意大小的integer性以及类还所有的数学基本运算。如下就是BigInteger版本的computIterativelyFaster。
public class Fibonacci {
public static BigInteger computeIterativelyFasterUsingBigInteger (int n)
{
if (n > 1) {
BigInteger a, b = BigInteger.ONE;
n--;
a = BigInteger.valueOf(n & 1);
n /= 2;
while (n-- > 0) {
a = a.add(b);
b = b.add(a);
}
return b;
}
return (n == 0) ? BigInteger.ZERO : BigInteger.ONE;
}
}
这个实现保证了溢出的情况不会再发生。又有新的问题产生了,如果调用computeIterativelyFasterUsingBigInteger(50000)耗费了差不多1.3秒完成,运行效果相当慢。造成这一原因有如下三点:
(1)BigInteger是不可变的。
因为BigInteger是不可变的,我们必须这样写“a=a.add(b)”而不是简单的“a.add(b)”。许多人经常误认为a.add(b)等于"a+=b".其实它是等于a+b。因此,我们必须这样写:a.add(b).然而,就是这一操作,又创建出一个新的对象。
(2)BigInteger是使用BigInt和本地代码实现的。
因为BigInteger的外部实现,另外的yigBigInt对象将被在每一次的BigInteger操作运算中创建。结果就是创建比预期多一倍的对象。例如:computeIterativelyFasterUsingBigInteger(50000)执行一次,将创建大约100,000个对象(虽然可以在最后被垃圾处理机制收集)。而且,BigInt是利用本地代码实现的(调用本地代码还需要JIT编译器一定的开销)。
(3)越大的数字,加起来的操作就更长。
非常大的数字不匹配单独的,长整形的64-bit值。例如,50,000Fibonacci数是34,7111-bit。
(友情提示:BitInteger这样的外部实现(BigInteger.java)将会在未来的Android发行版改变。实际上,外部实现的任何类都会改变。)
内存的分配十分重要,例如,当用到不可变的对象如BigInteger。下一步的优化应通过另一种算法减少内存的分配,基于Fibonacci Q矩阵,如下:
这两方程可以再次用BigInteger实现,如下:
public class Fibonacci {
public static BigInteger computeRecursivelyFasterUsingBigInteger (int n)
{
if (n > 1) {
int m = (n / 2) + (n & 1); // not obvious at first – wouldn’t it be great to
have a better comment here?
BigInteger fM = computeRecursivelyFasterUsingBigInteger(m);
BigInteger fM_1 = computeRecursivelyFasterUsingBigInteger(m - 1);
if ((n & 1) == 1) {
// F(m)^2 + F(m-1)^2
return fM.pow(2).add(fM_1.pow(2)); // three BigInteger objects created
} else {
// (2*F(m-1) + F(m)) * F(m)
return fM_1.shiftLeft(1).add(fM).multiply(fM); // three BigInteger
objects created
}
}
return (n == 0) ? BigInteger.ZERO : BigInteger.ONE; // no BigInteger object
created
}
public static long computeRecursivelyFasterUsingBigIntegerAllocations(int n)
{
long allocations = 0;
if (n > 1) {
int m = (n / 2) + (n & 1);
allocations += computeRecursivelyFasterUsingBigIntegerAllocations(m);
allocations += computeRecursivelyFasterUsingBigIntegerAllocations(m - 1);
// 3 more BigInteger objects allocated
allocations += 3;
}
return allocations; // approximate number of BigInteger objects allocated when
computeRecursivelyFasterUsingBigInteger(n) is called
}
}
一次调用computeRecursivelyFasterUsingBigInteger(50000)消耗大约1.6秒。这个实现比我们目前的迭代方法还要慢。这一次,被分配的数量为大约200,000个对象。
(友情提醒:实际被创建的对象没那么多,因为BigInteger‘的实现使用预分配对象,例如 BigInteger.ZERO , BigInteger.ONE, or BigInteger.TEN,有些运算没必要分配新的对象,你可以查看android帮助文档了解具体的实现细节:http://developer.android.com/reference/java/math/BigInteger.html)
结下做进一步优化,虽然用BigInteger能保证正确性,但也没必要对于每一个n值使用BigInteger。我们知道原始类似的长整形最多能达到92,我们可以做点小小的改动,把Biginteger和原始类型混合使用,如下所示:
public class Fibonacci {
public static BigInteger computeRecursivelyFasterUsingBigIntegerAndPrimitive(int n)
{
if (n > 92) {
int m = (n / 2) + (n & 1);
BigInteger fM = computeRecursivelyFasterUsingBigIntegerAndPrimitive(m);
BigInteger fM_1 = computeRecursivelyFasterUsingBigIntegerAndPrimitive(m -
1);
if ((n & 1) == 1) {
return fM.pow(2).add(fM_1.pow(2));
} else {
return fM_1.shiftLeft(1).add(fM).multiply(fM); // shiftLeft(1) to
multiply by 2
}
}
return BigInteger.valueOf(computeIterativelyFaster(n));
}
private static long computeIterativelyFaster(int n)
{
// see Listing 1–5 for implementation
}
}
调用 computeRecursivelyFasterUsingBigIntegerAndPrimitive(50000)耗费73毫秒,被分配大约11,000个对象。显而易见,相对于上一个,这次小小的改动比上一个快了20倍,而且比上一个少分配了20倍的对象空间。很惊奇是吧!其实,我们还可以通过减少分配空间的数量做进一步优化,当Fibonacci类第一次被载入,预先计算的结果能很快产生,这一结果能再下一次被立即使用,如下所示:
public class Fibonacci {
static final int PRECOMPUTED_SIZE= 512;
static BigInteger PRECOMPUTED[] = new BigInteger[PRECOMPUTED_SIZE];
static {
PRECOMPUTED[0] = BigInteger.ZERO;
PRECOMPUTED[1] = BigInteger.ONE;
for (int i = 2; i < PRECOMPUTED_SIZE; i++) {
PRECOMPUTED[i] = PRECOMPUTED[i-1].add(PRECOMPUTED[i-2]);
}
}
public static BigInteger computeRecursivelyFasterUsingBigIntegerAndTable(int n)
{
if (n > PRECOMPUTED_SIZE - 1) {
int m = (n / 2) + (n & 1);
BigInteger fM = computeRecursivelyFasterUsingBigIntegerAndTable (m);
BigInteger fM_1 = computeRecursivelyFasterUsingBigIntegerAndTable (m - 1);
if ((n & 1) == 1) {
return fM.pow(2).add(fM_1.pow(2));
} else {
return fM_1.shiftLeft(1).add(fM).multiply(fM);
}
}
return PRECOMPUTED[n];
}
}
这一程序的运行需要PRECOMPUTED_SIZE(预处理数量):预计算数量设定越大,速度越快。然而,因为许多BigInteger 对象江北创建以及和直到Fibonacci类被加载保留在内存中,内存的利用又成了一个新的问题。我就合并上两个的算法,结合它们的优势使用。0~92可以使用 computeIterativelyFaster,93~127使用预处理结果,其他的就使用递归,而且,作为开发者,你要善于选择最好的实现,虽然不一定速度最快,我们还要基于以下考虑:
(1)你android应用目标适用的版本
(2)你的人力物力资源
我们注意到,优化可能使你的代码的可读性越来越差,所以,你应该想清楚是否有优化的必要以及它们会怎样影响你应用的发展。