关于斐波那契数列的优化

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

实现斐波那契数列

public class Fibonacci_1 {
	public static long computeRecursivelyWhitLoop(int n) {
		if (n > 1)
			return computeRecursivelyWhitLoop(n - 2)
					+ computeRecursivelyWhitLoop(n - 1);
		return n;
	}
}

从代码的角度来看,一目了然,写这么个程序不就分分钟的事么,但是当n的值过大时就能看出来了;可能在计算机上看不出什么但是如果是在真实平台上运行就能很明显的看出问题。在三星Galaxy Tab 10.1上计算第30项大约要花370毫秒(其实我没测过,书上搬的O(∩_∩)O哈!)禁用JIT编译器之后需要大约440毫秒。如果我们需要计算的数字结果不是“立马”输出来的那么用户体验是非常差的,用户不会在乎你的过程。

第一步优化 思路:消除方法的调用但是最终还是用到的递归,仅仅只是减少了调用的次数

public class Fibonacci_2 {

	public static long computeRecursivelyWithLoop(int n) {
		if (n > 1) {
			long result = 1;
			do {
				result += computeRecursivelyWhitLoop(n - 2);
				n--;
			} while (n > 1);
			return result;
		}
		return n;
	}
}

在这里当n=30时computeRecursively(30)产生了2692537次调用computeRecursivelyWithLoop(30)却只产生了1346269次,当然还是使用了270毫秒,这样的优化还达不到我们要求,那么继续!

第二次优化我们会使用迭代实现:

public class Fibonacci_3 {

	public static long computeInteratively(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=30花了不到一毫秒,n=5000只要两毫秒,n=500000大概会花20到30毫秒。

到这里其实斐波那契数列的优化已经达标了,但是改变算法还可以让它更快!因为原算法的迭代次数可能是奇数,所以a和b的初始值要做相应的修改:该数列开始时如果n是奇数,则a=0,b=1,;如果n是偶数,则a=1,b=1(Fib(2)=1);代码如下:

public class Fibonacci_4 {
	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;
	}
}

值得注意的是这样的迭代速度回比之前的快了一倍。 但是有一个大问题,结果可能不是正确的,问题原因在于返回值long型只有64位。 在有符号的64位值得范围内可容纳的斐波那契数列的第92项。虽然程序不会崩溃但是数据会溢出第93项会为负。当然递归也是如此,下面还需要接着优化当然递归也是如此,下面还需要接着优化。

既然数据长度不够,就需要找一个能容纳它的类型来。java提供了一个java.math.BigInteger。其可以容纳任意大小的有符号整数,代码如下:

public class Fibonacci_5 {
<span style="white-space:pre">	</span>public static BigInteger computeIterativelyFasterUsigBigInteger(int n) {
<span style="white-space:pre">		</span>if (n > 1) {
<span style="white-space:pre">			</span>BigInteger a, b = BigInteger.ONE;
<span style="white-space:pre">			</span>n--;
<span style="white-space:pre">			</span>a = BigInteger.valueOf(n & 1);
<span style="white-space:pre">			</span>n /= 2;
<span style="white-space:pre">			</span>while (n-- > 0) {
<span style="white-space:pre">			</span>a = a.add(b);
<span style="white-space:pre">			</span>b = b.add(a);
<span style="white-space:pre">			</span>}
<span style="white-space:pre">			</span>return b;
<span style="white-space:pre">		</span>}
<span style="white-space:pre">		</span>return (n == 0) ? BigInteger.ZERO : BigInteger.ONE;
<span style="white-space:pre">	</span>}
}


 数字是不会溢出了但是数度却会满下来,因为BigInteger是不可变的,并且得考虑到BigInteger是使用的BigInt和本地代码实现的,数字越大,相加运算这一步的时间也就越长每一次相加add就相当于创建了一个新的BigInteger对象,当n=50000时花了1.3秒;创建了100000个对象;那么接下来的优化就要放在如何减少对象的创建上了。

运用到了斐波那契 Q-矩阵(其实我不懂,我只是搬运工);代码如下:

public class Fibonacci_6 {

	public static BigInteger computeIterativelyFasterUsigBigInteger(int n) {
		if (n > 1) {
			int m = (n / 2) + (n & 1);
			BigInteger fm = computeIterativelyFasterUsigBigInteger(m);
			BigInteger fm_1 = computeIterativelyFasterUsigBigInteger(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 (n == 0) ? BigInteger.ZERO : BigInteger.ONE;
	}

	// 实际分配的数量比computeIterativelyFasterUsigBigIntegerAllocations返回的估算值少,
	// 因为BigInteger使用了预分配对象如BigInteger.ZERO : BigInteger.TEN,有一些是没必要分配对象的
	public static long computeIterativelyFasterUsigBigIntegerAllocations(int n) {
		long allocations = 0;
		if (n > 1) {
			int m = (n / 2) + (n & 1);
			allocations += computeIterativelyFasterUsigBigIntegerAllocations(m);
			allocations += computeIterativelyFasterUsigBigIntegerAllocations(m - 1);
			allocations += 3;// 创建的BigInteger对象多于三个
		}
		return allocations;// 当掉用computeIterativelyFasterUsigBigInteger(n)时,创建BigInteger对象的近似数
	}
}
在这里我们会发现最终我们还是觉得运行没有用long时快,那么其实我们可以让long使用只是在超出其范围时使用BigInteger这是一种思维,要会用!!!代码如下:

public class Fibonacci_7 {
	public static BigInteger computeIterativelyFasterUsigBigIntegerAndPrimitive(
			int n) {
		if (n > 92) {
			int m = (n / 2) + (n & 1);
			BigInteger fm = computeIterativelyFasterUsigBigIntegerAndPrimitive(m);
			BigInteger fm_1 = computeIterativelyFasterUsigBigIntegerAndPrimitive(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 BigInteger.valueOf(computeIterativelyFaster(n));
	}

	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;
	}
}
这里调用当n=50000花了约73毫秒,创建了11000个对象;略微修改下算法,速度快了近20倍,创建对象则仅仅是原来的1/20!通过减少创建对象的数量,进一步改善是可行的,那么首次加载时,先快速生成预先计算的结果,这些结果以后就可以直接使用。

public class Fibonacci_8 {
	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 computeIterativelyFasterUsigBigIntegerAndTable(
			int n) {
		if (n > PRECOMPUTED_SIZE - 1) {
			int m = (n / 2) + (n & 1);
			BigInteger fm = computeIterativelyFasterUsigBigIntegerAndTable(m);
			BigInteger fm_1 = computeIterativelyFasterUsigBigIntegerAndTable(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];
	}
}

到这里算是差不多了,速度有多快就看你的size有多大,但是新的问题也接踵而至,因为BigInteger对象创建后保存在内存中,只要加载Fibonacci类,它们就会占用内存,这里就需要灵活应变了,例如当计算n=92时可以使用Fibonacci_7当计算93~127项时可以使用预加载计算结果,其它的使用递归,这里应该很明白了,一种方法不是说仅仅就是一种做法就实现完全,可以多个方法交替,互相补足来实现,软件开发也是如此我们有权利选择最优解而不仅仅是追求速度。当一个软件目前要出版本,功能都没有完全实现,你会去想着优化么?所以找到适合自己的解决方案才是正解,最优解!
既然想到了预加载,那么久预加载的彻底点吧,我们将算出来的结果都存起来,下次用就不需要再算了这里用到了缓存。代码如下:

import java.math.BigInteger;
import android.util.SparseArray;

/**
 * 使用缓存优化菲波拉契数列
 * 如果是java代码实现可能会使用HashMap充当缓存,但是android定义了SparseArray类,当键是整数时,它比HashMap效率高,
 * 因为HashMap使用的是java
 * .lang.Integer对象,而SparseArray使用的是基本类型int。因此使用HashMap会创建很多的Integer对象
 * ,而SparseArray则可以避免 当然这是类依赖于android如果是java当然还是使用HashMap
 * 
 * @author zhu
 *
 */
public class Fibonacci_9 {
	public static BigInteger computeRecursivelyWithCache(int n) {
		SparseArray<BigInteger> cache = new SparseArray<BigInteger>();
		return computeRecursivelyWithCache(n, cache);
	}

	private static BigInteger computeRecursivelyWithCache(int n,
			SparseArray<BigInteger> cache) {
		if (n > 92) {
			BigInteger fn = cache.get(n);
			int m = (n / 2) + (n & 1);
			BigInteger fm = computeRecursivelyWithCache(m, cache);
			BigInteger fm_1 = computeRecursivelyWithCache(m - 1, cache);
			if ((n & 1) == 1) {
				fn = fm.pow(2).add(fm_1.pow(2));
			} else {
				fn = fm_1.shiftLeft(1).add(fm).multiply(fm);
			}
			return fn;
		}
		return BigInteger.valueOf(computeIterativelyFaster(n));
	}

	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;
	}
}
其实真正来说,菲波拉契数列我能写出来的就是递归,迭代我可能临时都想不起来,今天写这些主要是今天面试碰到了,我写了一个递归但是面试官问道了关于计算时间的问题,而我又恰好记得我有看过,但是始终想不起来怎么去优化,而且搜索了下也没有找到相关的代码优化,目前也就在《Android应用性能优化》一书中有看到过,本文的所有代码全部来自于本书(不知道会不会侵权。。。)好记性不如烂笔头,代码是要靠敲的,记下来,可以随时看,其实主要不是代码如何去写,而是理解其中的思想,想法很重要!如果在面试中我相信给你的时间你直接写出最优解面试官信不信是两说,当然你的技术够牛逼我不解释!至少一般的程序猿,装13装的差不多就够了,O(∩_∩)O哈哈~

就写到这里,慎用!

        --------------------------------------------------------本文所有代码来自《Android应用性能优化》



  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值