算法与数据结构-递归

文章目录

  • 什么是递归
  • 递归需要满足的三个条件
  • 递归可能存在的问题
    • 堆栈溢出
    • 重复计算
  • 总结


什么是递归

  递归是一种直接或者间接调用自身函数或者方法的算法(或者编程技巧),应用非常广泛。我们举个例子来说明什么是递归:

  推荐注册返佣金的这个功能我想你应该不陌生吧?现在很多 App 都有这个功能。这个功能中,用户 A 推荐用户 B 来注册,用户 B 又推荐了用户 C 来注册。我们可以说,用户 C 的“最终推荐人”为用户 A,用户 B 的“最终推荐人”也为用户 A,而用户 A 没有“最终推荐人”。

  一般来说,我们会通过数据库来记录这种推荐关系。在数据库表中,我们可以记录两行数据,其中 actor_id 表示用户 id,referrer_id 表示推荐人 id。
算法与数据结构-递归_第1张图片
  基于这个背景,给定一个用户 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)

这个算法我们用图示来看的话会是这样的:
算法与数据结构-递归_第2张图片
  可以看到,当计算f(6)的时候,会涉及到多次f(3)运算。这种重复计算的问题应该如何解决呢?我们可以在递归中,将运算结果缓存到Map里来避免重复计算的问题。

总结

  递归是一种非常高效、简洁的编码技巧。只要是满足“三个条件”的问题就可以通过递归代码来解决。

  递归代码虽然简洁高效,但是,递归代码也有很多弊端。比如,堆栈溢出、重复计算、函数调用耗时多、空间复杂度高等,所以,在编写递归代码的时候,一定要控制好这些副作用。

你可能感兴趣的:(算法与数据结构,算法,数据结构)