如何写递归

目录

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

从下到上

从上到下(尾递归)

优化递归函数

再谈由上到下、由下到上

总结

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


出自:力扣

编程语言中,函数 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) + n中我们把 f(n-1)的结果(中间变量)提取出来 f(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) ,如果直接采用该公式进行递归会重复计算很多表达式。
  • 分析数据溢出问题
  • 将「由下到上」优化为「由上到下」,再改写为尾递归,再退化为循环结构。因为递归会对栈及中间变量的状态保存有额外的开销。

再谈由上到下、由下到上

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

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

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

总结

简单的总结为:

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

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

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

力扣

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: MySQL中可以通过使用WITH RECURSIVE语句来编递归查询。这种类型的查询可以在一个表的列中包含自引用值时使用,以便遍历和检索相关的数据。 下面是一个示例查询,假设有一个employees表,其中包含每个雇员的ID和经理ID。查询将返回给定雇员的所有下属: ``` WITH RECURSIVE subordinates AS ( SELECT id, name, manager_id FROM employees WHERE id = {employee_id} UNION SELECT e.id, e.name, e.manager_id FROM employees e JOIN subordinates s ON e.manager_id = s.id ) SELECT * FROM subordinates; ``` 在这个例子中,查询使用了递归CTE(公共表表达式)subordinates。该CTE首先选择指定员工的ID、姓名和经理ID。然后,查询递归地加入每个直接下属的ID、姓名和经理ID,直到没有更多的下属可添加为止。最后,查询从subordinates CTE中选择所有行,以便返回给定雇员的所有下属。 ### 回答2: MySQL可以使用WITH语句来实现递归查询。在WITH语句中,首先定义一个递归出口,然后在递归部分引用自身,直到满足递归出口条件为止。 以下是一个使用WITH语句实现递归查询的示例: WITH RECURSIVE cte AS ( SELECT id, parent_id, name FROM your_table WHERE id = <起始ID> UNION ALL SELECT t.id, t.parent_id, t.name FROM your_table t INNER JOIN cte ON t.parent_id = cte.id ) SELECT * FROM cte; 上述代码中的`your_table`为你要查询的表名,`id`为主键列名,`parent_id`为父节点列名,`name`为节点名称列名。 首先,通过WHERE子句指定起始节点ID,作为递归查询的起点。 然后,在UNION ALL子句中使用SELECT语句引用自身,通过INNER JOIN连接两个表,从而继续查询下一个层级的节点。 最后,在主查询中选择所有层级的节点信息。 通过以上步骤,就可以实现递归查询。请注意,使用递归查询可能会导致性能问题,请谨慎使用,并确保表结构以及操作的合理性。 ### 回答3: MySQL本身是不支持递归查询的,但我们可以通过使用存储过程或者触发器来实现递归查询。 一种常见的方法是使用存储过程。我们可以创建一个存储过程,其中包含递归调用自身的逻辑。在每次递归调用中,我们可以通过传递参数来指定递归的条件,并在每次递归调用中更新这些参数。通过递归调用,我们可以在每次调用中查询满足递归条件的数据,并将结果作为参数传递给下一次递归调用。这样就可以实现递归查询。 另一种方法是使用触发器。我们可以创建一个触发器,在每次插入或更新数据时触发。在触发器中,我们可以通过查询满足递归条件的数据,并将结果插入到临时表中。然后,我们可以在下一次触发器被触发时再次查询临时表,并将结果插入到临时表中。通过多次触发,我们可以实现递归查询。 无论是使用存储过程还是触发器,递归查询都需要谨慎使用,因为递归查询可能会导致性能问题,并且可能存在死循环的风险。在实现递归查询时,我们需要仔细考虑递归的退出条件和递归的深度,以确保查询的正确性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值