前言
在上一篇开始Java8之旅(六) -- 使用lambda实现Java的尾递归中,我们利用了函数的懒加载机制实现了栈帧的复用,成功的实现了Java版本的尾递归,然而尾递归的使用有一个重要的条件就是递归表达式必须是在函数的尾部,但是在很多实际问题中,例如分治,动态规划等问题的解决思路虽然是使用递归来解决,但往往那些解决方式要转换成尾递归花费很多精力,这也违背了递归是用来简洁地解决问题这个初衷了,本篇介绍的是使用备忘录模式来优化这些递归,并且使用lambda进行封装,以备复用。
回顾
为了回顾上一章节,同时用本章的例子作对比,我们这里使用经典的斐波那契数列求解问题作为例子来讲解。
斐波那契数列表示这样的一组数 1,1,2,3,5,8,13.... 其表现形式为数列的第一个和第二个数为1,其余的数都是它前两位数的和,用公式表示为
\[ a_n =\left\{
\begin{aligned}
1, n <= 1\\
a_{n-1} + a_{n-2} , n > 1
\end{aligned}
,n\in N
\right.\]
递归求解
这里我们依据上面的数列公式直接使用递归解法求解该问题
/**
* 递归求解斐波那契数列
*
* @param n 第n个斐波那数列契数
* @return 斐波那契数列的第n个数
*/
public static long fibonacciRecursion(long n) {
if (n <= 1) return 1;
return fibonacciRecursion(n - 1) + fibonacciRecursion(n - 2);
}
/**
* 递归测试斐波那契数列
*/
@Test
public void testFibonacciRec() {
long start = System.nanoTime();
System.out.println(fibonacciRecursion(47));
System.out.printf("cost %.2f ms %n", (System.nanoTime() - start) / Math.pow(10, 6));
}
这里我们测试当n等于47的时候,所要花费的时间
4807526976
cost 13739.30 ms
Process finished with exit code 0
可以看出,递归的写法虽然简洁,但是消耗的时间是成指数级的。
尾递归求解
这里回顾上一章内容,使用尾递归求解,具体这里的尾递归接口的实现这里就不贴出来了(点击这里查看),下面是尾递归的具体调用代码,增加两个变量分别保存\(a_{n-2}\)与\(a_{n-1}\) 在下面的形参对应的分别是accPrev与accNext,尾递归是自底向上的,你可以理解成迭代的方式,每次调用递归将\(a_{n-1}\)赋值给\(a_{n-2}\),将\(a_{n-1} + a_{n-2}\) 赋值给 \(a_{n-1}\)
/**
* 尾递归求解斐波那契数列
* @param accPrev 第n-1个斐波那契数
* @param accNext 第n个斐波那契数
* @param n 第n个斐波那契数
* @return 包含了一系列斐波那契的完整计算过程,调用invoke方法启动计算
*/
public static TailRecursion fibonacciRecursionTail(final long accPrev,final long accNext, final long n) {
if (n <= 1) return TailInvoke.done(accNext);
return TailInvoke.call(() -> fibonacciRecursionTail(accNext, accPrev + accNext, n - 1));
}
/**
* 尾递归测试斐波那契数列
*/
@Test
public void testFibonacciTailRec() {
long start = System.nanoTime();
System.out.println(fibonacciRecursionTail(1,1,47).invoke());
System.out.printf("cost %.2f ms %n", (System.nanoTime() - start) / Math.pow(10, 6));
}
同样测试当n等于47的时候,所要花费的时间
4660046610375530309
cost 97.67 ms
Process finished with exit code 0
可以看出花费的时间是线性级别的,但是因为这里的尾递归是手动封装的,所以接口类的建立以及lambda表达式的调用等一些基本开销占用了大部分的时间,但是这是常数级别的时间,计算过程本身几乎不花费什么时间,所以性能也是十分好的。
迭代求解
尾递归在优化之后在计算过程上就变成了自底向上,因此也就是转变成了迭代的过程,这里大家配合迭代求解来理解尾递归求解应该会容易许多。
/**
* 斐波那契的迭代解法,自底向上求解
* @para