基础递归案例:斐波那契数列
-
之前的例子是每个递归函数只包含一个自身的调用,这称之为 single recursion
-
如果每个递归函数例包含多个自身调用,称之为 multi recursion
递归关系
下面的表格列出了数列的前几项
实现
public static int f(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return f(n - 1) + f(n - 2);
}
执行流程
递归优化-记忆法
上述代码存在很多重复的计算,例如求 f(5) 递归分解过程
可以看到(颜色相同的是重复的):
f(3) 重复了 2 次
f(2) 重复了 3 次
f(1) 重复了 5 次
f(0) 重复了 3 次
随着 n 的增大,重复次数非常可观,如何优化呢?
Memoization 记忆法(也称备忘录)是一种优化技术,通过存储函数调用结果(通常比较昂贵),当再次出现相同的输入(子问题)时,就能实现加速效果,改进后的代码
public static void main(String[] args) {
int n = 13;
int[] cache = new int[n + 1];
Arrays.fill(cache, -1);
cache[0] = 0;
cache[1] = 1;
System.out.println(f(cache, n));
}
public static int f(int[] cache, int n) {
if (cache[n] != -1) {
return cache[n];
}
cache[n] = f(cache, n - 1) + f(cache, n - 2);
return cache[n];
}
优化后的图示,只要结果被缓存,就不会执行其子问题
-
改进后的时间复杂度为 O(n)
-
请自行验证改进后的效果
-
请自行分析改进后的空间复杂度
注意
记忆法是动态规划的一种情况,强调的是自顶向下的解决
记忆法的本质是空间换时间
递归优化-尾递归
爆栈
举例:用递归做 n + (n-1) + (n-2) ... + 1
public static long sum(long n) {
if (n == 1) {
return 1;
}
return n + sum(n - 1);
}
在我的机器上 n = 12000 时,爆栈了,提示信息如下:
Exception in thread "main" java.lang.StackOverflowError
at Test.sum(Test.java:10)
at Test.sum(Test.java:10)
at Test.sum(Test.java:10)
at Test.sum(Test.java:10)
at Test.sum(Test.java:10)
...
为什么呢?
每次方法调用是需要消耗一定的栈内存的,这些内存用来存储方法参数、方法内局部变量、返回地址等等
方法调用占用的内存需要等到方法结束时才会释放
而递归调用我们之前讲过,不到最深不会回头,最内层方法没完成之前,外层方法都结束不了
例如,sum(3) 这个方法内有个需要执行 3 + sum(2),sum(2) 没返回前,加号前面的 3 不能释放
看下面伪码
long sum(long n = 3) { return 3 + long sum(long n = 2) { return 2 + long sum(long n = 1) { return 1; } } }
在解决爆栈问题之前呢?我们要先来了解一下下面的概念:
尾调用
如果函数的最后一步是调用一个函数,那么称为尾调用,例如
function a() {
return b()
}
下面三段代码不能叫做尾调用
function a() {
const c = b()
return c
}
-
因为最后一步并非调用函数
function a() {
return b() + 1
}
-
最后一步执行的是加法
function a(x) {
return b() + x
}
-
最后一步执行的是加法
为何尾递归才能优化?
调用 a 时
a 返回时发现:没什么可留给 b 的,将来返回的结果 b 提供就可以了,用不着我 a 了,我的内存就可以释放
调用 b 时
b 返回时发现:没什么可留给 c 的,将来返回的结果 c 提供就可以了,用不着我 b 了,我的内存就可以释放
如果调用 a 时
不是尾调用,例如 return b() + 1,那么 a 就不能提前结束,因为它还得利用 b 的结果做加法
尾递归
尾递归是尾调用的一种特例,也就是最后一步执行的是同一个函数
本质上,尾递归优化是将函数的递归调用,变成了函数的循环调用
改循环避免爆栈
public static void main(String[] args) {
long n = 100000000;
long sum = 0;
for (long i = n; i >= 1; i--) {
sum += i;
}
System.out.println(sum);
}