线性递归的形象解释以及尾递归简述

2024-02-22 17:38

本文主要是介绍线性递归的形象解释以及尾递归简述,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引言:递归函数是一个自我调用的函数。而当递归调用是整个函数体中最后执行的语句并且它的返回值不用于任何表达式的一部分时,这个递归调用就是尾递归。——定义虽然准确而精炼,但往往看起来不像人话


限于笔者水平,本文对递归的解释并不深入,欢迎指正。


一、线性递归: “把大象装冰箱”(附代码):

  • 嫌麻烦可以直接看代码 ^.^ 。

    1. 假设你持有一枚大象,你想把它装进冰箱,这时候,你以为只需要做三件事:开门,放大象,关门。然而事情没这么简单。
    2. 冰箱里有一张纸条,写着一个数字9527。
    3. 我们假设开门的全套动作要包括:大喊“芝麻开门”,鼓掌,开门。
    4. 关门的全套动作包括:大喊纸条的数字,关门,并把纸条的数字减1。
    5. 你的目的除了搞定大象之外,还有——计算纸条的最终数字。
    6. 于是你试着第1次执行开门动作:“芝麻开门!”,啪啪啪,开门…
    7. 发现没有纸条,但是里面还有一个冰箱,于是你只好继续执行第2次开门的动作…
    8. 发现没有纸条,但是里面还有一个冰箱,于是你只好继续执行第3次开门的动作…
    9. 终于在你执行完第250次开门的动作之后,一个纸条(返回值)闪亮登场。于是你不再执行下一次开门,而是把大象放了进去。
    10. 纸条上写着“9527”。
    11. 你大喊“9527”,关上第一道门,并把纸条改成“9526”…
    12. 你大喊“9526”,关上第二道门,并把纸条改成“9525”…
    13. 最后你把所有门关上,程序结束,纸条上的数字变成了“9277”。
    14. PS:这里“放置大象”代表你想要在获取返回值的同时执行的动作[1],而纸条的数字代表你在最深层获取的返回值[2],“发现纸条”这个事件则是“递归终止条件”[3]。
  • 代码:为简短,终止条件改为n==2


public class E00_Recursive {private static int a;  // 如果变量定义在方法内部,那么每层递归都将建立一个新变量,造成更多开销。static int f(int n) {System.out.print("芝麻开门,");System.out.println("啪啪啪," + "开门");// 开始寻找返回值(找纸条)if (n == 2) {  // 发现纸条,递归终止 [3]System.out.println("------放大象------");  // [1]a = 9527;  // 返回值 [2]} elsea = f(n + 1) - 1;System.out.print("喊" + a);System.out.println(",关门");return a;}public static void main(String[] args) {System.out.println(f(0));}
}
/* 结果
芝麻开门, 啪啪啪, 开门
芝麻开门, 啪啪啪, 开门
芝麻开门, 啪啪啪, 开门
------放大象------
喊9527, 关门
喊9526, 关门
喊9525, 关门
9525
*/
其实在IDE里设置debug断点,进行单步调试,跑一个斐波那契之类的demo,也有助于理解。

扩展思考:试想如果在方法体内部有两个递归调用语句(比如斐波那契的线性递归),那么按理说会造成1个调2个,2个调4个这种指数级的开销。但是对此我有些不解,实际上,如果我们把两个调用看做程序的两条线路,那么在任何一次调用中,两条路线都不会同时执行,只有在一条路线结束并返回后,另一条才会执行,所以实际的开销仍然是线性变化的。留作以后研究。


二、进阶:尾递归(附代码)

嫌废话请直接看代码,嘿嘿

上面的例子,每一层递归调用时,都要保存上层函数的状态,去等待下一层的返回值。这势必对栈内存造成额外的开销。那么该如何解决呢?
(底层调用过程我也不懂,只是大概知其然)

那么既然如此,显然我们现在的目标就是:不让函数保存调用的状态,即是说:函数在向下一层递归前进后,上一层应该只等待返回值,而没有等待执行的其他操作。这种特点让编译器可以对栈的使用进行优化。

  • 那么想让函数具备这种特点,还要进行以下思考:
    • 首先,既然保证上层函数“只”等待返回值,那么f(n-1)-1就不合要求,因为它有等待执行的操作“减1”。这样,值就不能在方法体内部迭代了,咋办呢?
    • 聪明的前辈们想到了增加参数的办法,去代替迭代的表达式,见代码。
    • 但是即便解决了迭代值的问题,我们的关门动作又该如何执行呢?现在函数状态不保存,我们怎么一层一层地回溯来关门呢。
    • 答案是:解决不了,只能不关门。(如果有误希望大佬给个提示)
    • 那么废话不多说,直接上代码,为了简便,我们在n==3时终止递归:

public class E00_TailRecursive {static int recursive(int n, int a){System.out.println("开门动作");if (n == 3){System.out.println("放大象");return a; // n==3时,传入的a值已经是经过多次减1计算得到的9524了。} else// 参数里的a-1 一开始可能不太好理解。实际上相当于我们一开始就拿到纸条,每递归一次就减1。而n仅仅用来控制递归层级。return recursive(n + 1, a - 1);  // 递归调用是最后一行代码,且不属于表达式的一部分}public static void main(String[] args) {System.out.println(recursive(0, 9527));}
}
/* Output
开门动作
开门动作
开门动作
开门动作
放大象
9524*/

最后再看尾递归定义应该就更好理解了:当递归调用是整个函数体中最后执行的语句并且它的返回值不用于任何表达式的一部分时,这个递归调用就是尾递归。

线性递归和尾递归在开销方面的直观展示:

线性递归:保存每一步等待返回值

f(0)

{ f(1) - 1}

{ { f(2) -1 } - 1}

{ { { f(3) - 1 } -1 } - 1}

{ { { 9527 - 1 } -1 } - 1}

{ { 9526 -1 } - 1}

{ 9525 - 1}

9524

尾递归: 直接等价替换成另外一个调用,最终返回值其实是最后一个函数的返回值

recursive(0, 9527)

recursive(1, 9526)

recursive(2, 9525)

recursive(3, 9524) -> 9524

  • 这是我目前对递归的理解,感谢阅读,欢迎指正。

这篇关于线性递归的形象解释以及尾递归简述的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/736004

相关文章

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测 目录 时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测基本介绍程序设计参考资料 基本介绍 MATLAB实现LSTM时间序列未来多步预测-递归预测。LSTM是一种含有LSTM区块(blocks)或其他的一种类神经网络,文献或其他资料中LSTM区块可能被描述成智能网络单元,因为

线性回归(Linear Regression)原理详解及Python代码示例

一、线性回归原理详解         线性回归是一种基本的统计方法,用于预测因变量(目标变量)与一个或多个自变量(特征变量)之间的线性关系。线性回归模型通过拟合一条直线(在多变量情况下是一条超平面)来最小化预测值与真实值之间的误差。 1. 线性回归模型         对于单变量线性回归,模型的表达式为:         其中: y是目标变量。x是特征变量。β0是截距项(偏置)。β1

C++ 重建二叉树(递归方法)

/*** struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* };*/#include <vector>class Solution {public:/*** 代码

大学生自救数据结构与算法(py实现)——01递归

目录 目录 递归 基本概念 工作原理 基本要素 优点 缺点 实现技巧 实例解析:计算阶乘 斐波那契数列 高效的斐波那契数列 python中的最大递归深度 二分查找 基本原理 性能分析 优化与变体 线性递归  元素序列的递归求和 二路递归 二路递归的基本概念 典型应用 工作原理 多重递归  示例:计算卡特兰数(Catalan Number) 尾递

DataWhale机器学习——第三章线性模型笔记

读书笔记: 《机器学习》第三章 线性模型 3.1 基本形式 3.1.1 线性模型的定义 3.1.2 线性模型的优点 简单易理解计算效率高容易实现和解释 3.1.3 线性模型的局限性 只能表达线性关系对于复杂的非线性关系,表现较差 3.2 线性回归 3.2.1 基本概念 3.2.2 最小二乘法 正规方程 3.2.3 正则化 为防止过拟合,可以在损失函数中加入正

对递归执行过程的简单描述

1. 分析代码 #include <stdio.h>void fun(int n){printf("1th - Level: %d Address: %d\n", n, &n);if(n < 3)fun(n+1);printf("2th - Level: %d Address: %d\n", n, &n);}int main(){fun(1);return 0;} 输出结果为:

vue+elementui搭建后台管理界面(5递归生成侧栏路由) vue定义定义多级路由菜单

有一个菜单树,顶层菜单下面有多个子菜单,子菜单下还有子菜单。。。 这时候就要用递归处理 1 定义多级菜单 修改 src/router/index.js 的 / 路由 {path: '/',redirect: '/dashboard',name: 'Container',component: Container,children: [{path: 'dashboard', name: '首

汉诺塔问题的java递归实现

import java.util.Scanner;public class Hanoi {int count=0;public void hanoi(int n,char A,char B,char C){ //把n个盘子移动到ccount++;if(n==1){System.out.println("盘子1从"+A+"移动到"+C); //再把最下边那个最大的盘子移到目标柱c上}el

二叉树的先序创建,先序,中序,后序的递归与非递归遍历,层次遍历,叶子结点数及树的深度

二叉树的先序创建,先序,中序,后序的递归与非递归遍历,层次遍历,叶子结点数及树的深度计算 输入格式:如   abd###ce##f##*   package tree;//二叉树的二叉链表实现import java.util.LinkedList;import java.util.Queue;import java.util.Scanner;import java.util.Sta

matlab fspecial 用法解释

fspecial 函数用于建立预定义的滤波算子,其语法格式为: h = fspecial(type) h = fspecial(type , para) 其中 type 指定算子的类型, para 指定相应的参数; type 的类型有: 1 、 'average' averaging filter 为均值滤波,参数为 hsize 代表模板尺寸