斐波那契(Fibonacci)的算法优化

前言

 斐波那契 相信大多数开发者都有所了解,也被人成为“兔子数列”。就是像这样的数列:0、1、1、2、3、5、8、13、21、34、55

如果您还没有想起或者以前没有接触过,可以参考链接:斐波那契百度百科 斐波那契维基百科

好了现在假设你知道了斐波那契的现象,那么进入正题:如何牛逼的用Java语言实现斐波那契

a)最常想到的算法

	/**
	 * 最常想到的 递归
	 * @param n
	 * @return
	 */
	private static long compute(int n){

		if(n>1)return compute(n-2)+compute(n-1);

		return n;
	}
注意微小优化:当n等于0或1时直接返回n,而不是另加一个if语句来检查n是否等于0或1。
调用:
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		long old = System.currentTimeMillis();
		System.out.println("结果 :"+compute(10));
		System.out.println("耗时 : "+(System.currentTimeMillis()-old));

	}
当n数量级为 10时,耗时是一瞬间
当n数量级为 100时,我在阳台上晒了会太阳还没算好
当n数量级为1000时,我已经绝望了。
Ps:大家可能都知道对用户而言,运算时长小于100ms就可以说是立马马上,没有任何卡顿。

b)就在a递归的基础上进行优化

	/**
	 * 稍微优化了的递归,减少了调用次数
	 * @param n
	 * @return
	 */
	private static long computeWithLoop(int n){


		if(n>1){
			long result = 1;
			do {

				result += computeWithLoop(n-2);
				n--;

			} while (n>1);

			return result;
		}

		return n;

	}
调用后发现:

虽然调用自身方法的次数少了多次,比如当n为30时 a方案调用本身2692537次,b方案调用本身“只有”1346269次

但是,对用户而言此方案的耗时依然无法接受

c)从递归到迭代

	/**
	 * 迭代算法 线性的速度很快
	 */
	private static long computeWithDieDai(int n){

		if(n>1){

			long a = 0,b = 1;

			do {

				long temp = b;
				b += a;
				a = temp;

			} while (--n>1);

			return b;
		}

		return n;
	}	
调用后发现 这个算法碉堡了,不管多达数 几乎都是瞬间的事情
当n数量级为 千万级别依旧是可以接受的
结果 :-4307732722963583941
耗时 : 272
这种迭代算法的复杂性大大降低,因为是线性的,其性能也很好,虽然这样已经答到了达标了,但相同的算法稍加修改后还可以更快:

d)优化后的迭代算法:

	/**
	 * 飞快的优化过的迭代算法
	 * @param n
	 * @return
	 */
	private static long computeWithDieDaiFaster(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;
	}
调用后发现
结果 :-4307732722963583941
耗时 : 69
竟然比方案c更快,实在是太爽了,本方案每次迭代计算两项,迭代总数少了一半,因为方案c的迭代次数可能是奇数,所以a和b的初始值要做相应的修改:该数列开始时如果n是奇数,则a = 0,b = 1;如果n是偶数,则a = 1,b = 1(Fib(2) = 1)
但是细心的你有没有发现不管是方案c还是方案d,最后得到的结果竟然是错误的,是的,问题就在于返回值是long型的,它只有64位。在有符号的64位值范围内,有效的最大斐波那契数为 7 540 113 804 746 346 429 ,或者说是斐波那契数列的第92项,虽然这些方案在计算超过92项时并没有让应用崩溃,但是溢出造成数据错误,也就是说斐波那契第93项是负数,其实方案a和方案b这样的递归算法也会出现同样的问题,只不过你得耐心的等待才能得知这一事实。
那有解决办法么,答案是 Yeah

e)用BigInteger解决溢出问题

	/**
	 * 用BigInteger解决大数据溢出问题
	 * @param n
	 * @return
	 */
	public static BigInteger computeWithBig(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;
	}

 
	long old = System.currentTimeMillis();
		System.out.println("结果 :"+computeWithBig(10000));
		System.out.println("耗时 : "+(System.currentTimeMillis()-old));
结果 :33644764876431783266621612005107543310302148460680063906564769974680081442166662368155595513633734025582065332680836159373734790483865268263040892463056431887354544369559827491606602099884183933864652731300088830269235673613135117579297437854413752130520504347701602264758318906527890855154366159582987279682987510631200575428783453215515103870818298969791613127856265033195487140214287532698187962046936097879900350962302291026368131493195275630227837628441540360584402572114334961180023091208287046088923962328835461505776583271252546093591128203925285393434620904245248929403901706233888991085841065183173360437470737908552631764325733993712871937587746897479926305837065742830161637408969178426378624212835258112820516370298089332099905707920064367426202389783111470054074998459250360633560933883831923386783056136435351892133279732908133732642652633989763922723407882928177953580570993691049175470808931841056146322338217465637321248226383092103297701648054726243842374862411453093812206564914032751086643394517512161526545361333111314042436854805106765843493523836959653428071768775328348234345557366719731392746273629108210679280784718035329131176778924659089938635459327894523777674406192240337638674004021330343297496902028328145933418826817683893072003634795623117103101291953169794607632737589253530772552375943788434504067715555779056450443016640119462580972216729758615026968443146952034614932291105970676243268515992834709891284706740862008587135016260312071903172086094081298321581077282076353186624611278245537208532365305775956430072517744315051539600905168603220349163222640885248852433158051534849622434848299380905070483482449327453732624567755879089187190803662058009594743150052402532709746995318770724376825907419939632265984147498193609285223945039707165443156421328157688908058783183404917434556270520223564846495196112460268313970975069382648706613264507665074611512677522748621598642530711298441182622661057163515069260029861704945425047491378115154139941550671256271197133252763631939606902895650288268608362241082050562430701794976171121233066073310059947366875
耗时 : 16
BigInteger对象可以容纳任意大小的有符号整数,这个方案当n>92时不会出现溢出成负数的情况,但是速度却又慢了下去

f)基于斐波那契Q-矩阵减少内存分配

	/**
	 * 通过斐波那契Q矩阵的公式减少生成的对象
	 * @param n
	 * @return
	 */
	private static BigInteger computeWithBigFaster(int n){

		if(n>92){

			int m = (n/2)+(n & 1);
			BigInteger fM = computeWithBigFaster(m);
			BigInteger fM_1 = computeWithBigFaster(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(computeWithDieDaiFaster(n));

	}
这个算法比较复杂,简单来说就是因为方案e中

由于BigInteger是不可变的,我们必须写a = a.add(b),而不是简单地用a.add(b),很多人误以为a.add(b)相当于a += b, 但实际上它等价于a + b。因此,我们必须写成a = a.add(b),把结果值赋给a。这里有个小细节是非常重要的:a.add(b)会创建一个新的BigInteger对象来持有额外的值。

目前BigInteger的内部实现,每分配一个BigInteger对象就会另外创建一个BigInt对象。在执行方案e时,要分配两倍的对象:调用computeWithBig(50000)时约创建了100 000个对象(除了其中的1个对象外,其他所有对象立刻变成等待回收的垃圾)。此外,BigInt使用本地代码,而从Java使用JNI调用本地代码会产生一定的开销。

所以本方案基于斐波那契Q矩阵有以下公式:

上面的代码就是根据这个公式写出的,虽然得出的结果依旧没有迭代的方法快,但是毕竟是朝正确的方向又迈出了一步。

最后既然基本类型Long可以容纳小于等于92的结果,我们稍微修改递归实现,混合BigInteger和基本类型(对我启发就是很多优秀的实现都不仅仅是单一的算法,因为各有优缺点,终于领悟以前上学时候的算法为什么一种一种又一种了),且看下面的综合算法

g)混合使用基本类型和BigInteger

	/**
	 * 混合使用基本类型和BigInteger优化
	 * @param n
	 * @return
	 */
	private static BigInteger computeWithBigFaster(int n){

		if(n>92){

			int m = (n/2)+(n & 1);
			BigInteger fM = computeWithBigFaster(m);
			BigInteger fM_1 = computeWithBigFaster(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(computeWithDieDaiFaster(n));

	}
调用后发现
略微修改下算法,速度就快了约20倍,创建对象数则仅是原来的1/20,很惊人吧!通过减少创建对象的数量,进一步改善性能是可行的
其实还有一种比较好的思路:

h)通过缓存已经计算过的数据提高性能

/**
	 * 每次计算过的数据存入缓存
	 * @param n
	 * @return
	 */
	private static BigInteger computeWithCache(int n){

		HashMap<Integer,BigInteger> cache = new HashMap<Integer,BigInteger>();

		return computeWithCache(n,cache);

	}

	private static BigInteger computeWithCache(int n,HashMap<Integer, BigInteger>cache){

		if(n>92){

			BigInteger fN = cache.get(n);
			if(fN==null){


				int m = (n/2)+(n & 1);
				BigInteger fM = computeWithCache(m, cache);
				BigInteger fM_1 = computeWithCache(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);
				}
				cache.put(n, fN);
			}

			return fN;

		}

		return BigInteger.valueOf(computeWithDieDaiFaster(n));

	}
因为Java作为编程语言,你可能打算使用一个HashMap充当缓存,它可以胜任这项工作。不过, Android定义了SparseArray(稀疏数组)类,当键是整数时,它比HashMap效率更高。因为HashMap使用的是java.lang.Integer 对象,而SparseArray使用的是基本类型int。因此使用HashMap会创建很多Integer对象,而使用SparseArray则可以避免
尽管使用HashMap代替SparseArray会慢一些, 不过我在这里依旧用HashMap进行缓存 , 这样的好处是可以让代码不依赖Android,也就是说,你可以在非Android的环境(无SparseArray)使用完全相同的代码。
注意 Android定义了多种类型的稀疏数组(sparse array):SparseArray(键为整数,值为对象)、SparseBooleanArray(键为整数,值为boolean)和SparseIntArray(键为整数,值为整数)
最后给出相关算法的全部Demo(不需要积分)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex_ChuTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值