淘先锋技术网

首页 1 2 3 4 5 6 7

目录

在实现递归函数之前,有两件重要的事情需要弄清楚:

从下到上

从上到下(尾递归)

优化递归函数

再谈由上到下、由下到上

总结

其余实战及练习题见开头链接出处


出自:https://leetcode-cn.com/circle/article/koSrVI/

编程语言中,函数 Func(Type a,……)直接或间接调用函数本身,则该函数称为「递归函数」。

在实现递归函数之前,有两件重要的事情需要弄清楚:

  • 递推关系:一个问题的结果与其子问题的结果之间的关系。
  • 基本情况(终止条件):不需要进一步的递归调用就可以直接计算答案的情况。可理解为递归跳出条件。
     

从下到上

由下到上:在每个递归层次上,我们首先递归地调用自身,然后根据返回值进行计算。(依赖返回值)

public int sum(int n) {
    if (n < 2) return n;       // ① 递归基本情况
    int childSum = sum(n - 1); // ② 寻找基本情况
    return n + childSum;       // ③ 根据返回值运算
}

由下到上-范式

  • 寻找递归递推关系
  • 寻找递归基本情况,跳出时返回基本情况的结果
  • 修改递归函数的参数
  • 递归调用并返回中间变量
  • 使用递归函数的返回值与当前参数进行计算,并返回最终结果

public 返回值 f(参数) {
    if (基本情况条件) return 基本情况的结果;       
    
    修改参数;
    返回值 = f(参数); 
    
    最终结果 = 根据参数与返回值计算
    return 最终结果;
}

从上到下(尾递归)

假如我们换个思路,f(n) = f(n-1) + nf(n)=f(n−1)+n 中我们把 f(n-1)f(n−1) 的结果(中间变量)提取出来 f(n, SUM) = SUM + nf(n,SUM)=SUM+n,
每次计算都带着它,这样我们可以先计算,然后把计算好的结果传递给递归函数进行下一次计算,这个过程我们称为「由上到下」。

由上到下:在递归层级中,我们根据当前「函数参数」计算出一些值,并在递归调用函数时将这些值传给自身。(依赖函数参数)

/**
 * 模拟程序执行过程:
 * sum(5, 0)
 * sum(4, 5)
 * sum(3, 9)
 * sum(2, 12)
 * sum(1, 14)
 * 15
 * <p>
 * 由上到下:最终从 5 + 4 + 3 + 2 + 1 计算...
 * 递归函数「末尾」部分调用自身,根据逻辑先进行计算,然后把计算的中间变量传递调用函数。
 * <p>
 * 这种在函数末尾调用自身的递归函数叫做「尾递归」
 */
public int sum2(int n, int sum) {
    if (n < 2) return 1 + sum;
    sum += n;
    return sum2(n - 1, sum);
}

由上到下-范式

  • 寻找递归递推关系
  • 创建新函数,将「由下到上-范式」中的最终结果计算依赖的中间变量提取为函数的参数
  • 寻找递归基本情况,跳出时返回基本情况的结果与中间变量的计算结果(最终结果)
  • 根据函数参数与中间变量重新计算出新的中间变量
  • 修改参数
  • 递归调用并返回(该处的返回由基本情况触发)
public 返回值 f(参数,中间变量) {
    if (基本情况条件) return 基本情况的结果与中间变量的计算结果;
    
    中间变量 = 根据参数与中间变量重新计算
    修改参数;
    
    return f(参数,中间变量);
}

优化递归函数

优化点总结为:

  • 充分分析基本情况(跳出条件),避免临界值跳不出递归,导致栈溢出。
  • 分析递归深度,太深的递归容易导致栈溢出。
  • 分析是否有重复计算问题,主要分析函数参数值是否会出现重复,直接代入递归的递推关系中运算即可。如果会出现重复使用数据结构记录,即备忘录。
  • 比如:斐波那契数列 f(n) = f(n-1) + f(n-2)f(n)=f(n−1)+f(n−2) ,如果直接采用该公式进行递归会重复计算很多表达式。
  • 分析数据溢出问题
  • 将「由下到上」优化为「由上到下」,再改写为尾递归,再退化为循环结构。因为递归会对栈及中间变量的状态保存有额外的开销。

再谈由上到下、由下到上

一般情况,我们说递归时指的是「由下到上」,因为「由上到下」的过程往往需要创建新函数去完成,更甚至「由上到下」其实就是循环结构封装为函数式编程的写法,也叫尾递归。
由下到上转换为由上到下的过程其实就是转换为循环结构写法的过程。

递归难以理解的地方在于由下到上的过程,其实细化该难点可以分为「基本情况」->「改变参数继续递归」->「拿到递归返回值与当前参数计算」。
实际编码中我们只要按上述提到的范式进行代码编写,上述示例中的基本情况比较单一,中间变量也只涉及一个,对于复杂的跳出及中间变量的处理只要按范式步骤进行分析然后再优化一定可以写出一个递归函数。

对于递推关系的寻找过程,没有范式可寻,需要见多识广(:hear_no_evil:刷刷刷:hear_no_evil:),不断总结。

总结

简单的总结为:

  • 你要写哪种类型的递归,从上算还是从下算,这决定了你如何确认递推关系
  • 分析基本情况
  • 寻找递推关系,在递推关系中提取中间变量
  • 套入上文中的递归范式
  • 按上文中的优化点进行优化

题外话:对于自上而下的计算不是必须创建新函数去传入中间变量,因为有时我们可以使用全局变量保存、或者直接修改当前递推关系中的变量即可。
推荐使用总结内容「由上到下、由下到上、循环结构」三种方法完成力扣-206. 反转链表

其余实战及练习题见开头链接出处

https://leetcode-cn.com/circle/article/koSrVI/