文章目录
- 什么是递归
- 递归需要满足的三个条件
- 递归可能存在的问题
- 堆栈溢出
- 重复计算
- 总结
什么是递归
递归是一种直接或者间接调用自身函数或者方法的算法(或者编程技巧),应用非常广泛。我们举个例子来说明什么是递归:
推荐注册返佣金的这个功能我想你应该不陌生吧?现在很多 App 都有这个功能。这个功能中,用户 A 推荐用户 B 来注册,用户 B 又推荐了用户 C 来注册。我们可以说,用户 C 的“最终推荐人”为用户 A,用户 B 的“最终推荐人”也为用户 A,而用户 A 没有“最终推荐人”。
一般来说,我们会通过数据库来记录这种推荐关系。在数据库表中,我们可以记录两行数据,其中 actor_id 表示用户 id,referrer_id 表示推荐人 id。
基于这个背景,给定一个用户 ID,如何查找这个用户的“最终推荐人”?递归算法就排上用场了。我们用代码来示例:
public long findRootReferrerId(long actorId) {
// 此处是伪代码
Long referrerId = select referrer_id from [table] where actor_id = actorId;
if (referrerId == null) return actorId;
return findRootReferrerId(referrerId);
}
可以看到,我们在findRootReferrerId方法内部调用了自身。这种算法(或编码技巧)就是递归。
递归需要满足的三个条件
-
一个问题的解可以分解为几个子问题的解
何为子问题?子问题就是数据规模更小的问题。比如,前面讲的电影院的例子,你要知道,“自己在哪一排”的问题,可以分解为“前一排的人在哪一排”这样一个子问题。 -
这个问题与分解之后的子问题,求解思路完全一样
比如上面的例子,求解“推荐人”的思路,和“推荐人”求解“推荐人的推荐人”的思路,是一模一样的。 -
存在递归终止条件
把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。还是上面的例子,当A的推荐人为空时,就说明A是最终推荐人。
递归可能存在的问题
堆栈溢出
在“算法与数据结构-栈”那一篇博文中讲过,函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
比如前面的讲到的查询最终推荐人的例子,如果我们将系统栈或者 JVM 堆栈大小设置为 1KB,在系统推荐层次达到了10000次的时候,就有可能报错:
Exception in thread “main” java.lang.StackOverflowError
那么,如何避免出现堆栈溢出呢?
- 1、限制递归调用的最大深度,递归调用超过一定深度(比如 1000)之后,我们就不继续往下再递归了,直接返回报错。
- 2、将递归算法改为非递归算法,限制循环次数。
重复计算
假如现在我们有一个递归算法,其推导公式如下:
f(n) = f(n-1) + f(n-2)
这个算法我们用图示来看的话会是这样的:
可以看到,当计算f(6)的时候,会涉及到多次f(3)运算。这种重复计算的问题应该如何解决呢?我们可以在递归中,将运算结果缓存到Map里来避免重复计算的问题。
总结
递归是一种非常高效、简洁的编码技巧。只要是满足“三个条件”的问题就可以通过递归代码来解决。
递归代码虽然简洁高效,但是,递归代码也有很多弊端。比如,堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等,所以,在编写递归代码的时候,一定要控制好这些副作用。