程序调用自身,称为递归。
递归是一个非常重要的算法思想,生活中也常见类似场景,比如排队时想知道前面还有几个人,需要向前问。再比如考试时学生向后传试卷,直到最后一个就将剩余的试卷还给老师。
什么样的情况下可以用递归?
(1)一个问题可以分解成多个子问题
(2)这个问题与分解成的子问题求解思路一致
(3)一定有一个终止条件
实现递归最核心的就是找到公式和终止条件。
以斐波那契数列举例:1 1 2 3 5 8 13
找公式,一个位置上的数字等于前两位数字之和,即f(n)=f(n-1)+f(n-2),终止条件:n<=2时f(n)=1
公式变成代码就很简单了:
public static int fab(int i) { if (i <= 2) { return 1; } return fab(i - 1) + fab(i - 2); }
输出前五个:1 1 2 3 5
但是这个递归的时间复杂度和空间复杂度非常高,为O(2^n),当尝试计算第40个数字就已经到了秒级。写段代码测试一下:
for (int i = 1; i < 50; i++) { long a = System.currentTimeMillis(); fab(i); long b = System.currentTimeMillis(); System.out.println("第" + i + "次耗时:" + (b - a)); }
第40次耗时:549 第41次耗时:1003 第42次耗时:1110 第43次耗时:1129 第44次耗时:1747 第45次耗时:2815 第46次耗时:4651
这种性能是我们不能容忍的,需要进行优化。最直接的是不使用递归,一般来说,递归都是可以使用别的办法解决的。比如这个地方,我们就用循环解决。
public static int loop(int n) { if (n <= 2){ return 1;
} int a = 1; int b = 1; int res = 0; for (int i = 3; i <= n; i++) { res = a + b; a = b; b = res; } return res; } 测试结果: 第46次耗时:0 第47次耗时:0 第48次耗时:0 第49次耗时:0
这样我们就优化到O(n)的复杂度。但是用循环又显得比较难看,我们追求更简洁的代码,还是递归更优雅,那之前的问题是同一个位置的数据计算过多,我们可以考虑加一层缓存,每次计算好了数据我们缓存起来,下一次可以直接取用。
private static int data[]; public static int cacheFab(int n) { if (n <= 2){ return 1; } if (data[n] > 0) { return data[n]; } int res = cacheFab(n - 1) + cacheFab(n - 2); data[n] = res; return res; } public static void main(String[] args) { int n = 50; data = new int[n]; for (int i = 1; i < n; i++) { long a = System.currentTimeMillis(); cacheFab(i); long b = System.currentTimeMillis(); System.out.println("第" + i + "次耗时:" + (b - a)); } } 第46次耗时:0 第47次耗时:0 第48次耗时:0 第49次耗时:0
从上面可以看到,性能依然很高。但是还是使用了数组缓存,还可以进一步优化,就是使用尾递归。尾递归就是调用函数出现在末尾,这时候就不会创建新的栈,而且覆盖到前面去。
/** * @param pre 上上次结果 * @param res 上次结果 * @param n * @return */ public static int tailFab(int pre, int res, int n) { if (n <= 2) { return res; } return tailFab(res, pre + res, n - 1); } public static void main(String[] args) { for (int i = 1; i < 50; i++) { long a = System.currentTimeMillis(); tailFab(1, 1, i); long b = System.currentTimeMillis(); System.out.println("第" + i + "次耗时:" + (b - a)); } } 第46次耗时:0 第47次耗时:0 第48次耗时:0 第49次耗时:0
这个性能也是O(n)的,代码简洁性能高,推荐用这种方式,如果工作中有递归的场景,可以尝试使用。