递归与尾递归

news2024/9/22 19:44:14

你肯定知道递归,大概也知道尾递归。可是为什么要在递归中分出尾递归这样一个类?尾递归的本质又是什么?许多人未必清楚。

递归原是Lisp语言提出的概念,后来被许多语言借鉴。递归指自我重复的结构,在编程语言中体现为函数的自我调用。递归在算法中有着非常重要的地位,在函数式编程中,递归是最基本的结构,没有循环,只有递归;而在命令式语言中,人人都知道要避免使用递归,因为深度递归会耗尽栈空间。

尾递归是递归中比较特殊的一种情况,经过优化的尾递归不会吞吃栈空间,而是使用固定大小的栈空间,如同循环一样。网上许多对于尾递归的解释是“在函数末尾调用自身”其实是错误的。在函数末尾调用自身不一定是尾递归,但尾递归一定是在末尾调用自身,在末尾调用自身只是尾递归的充分不必要条件。要解释清楚尾递归的本质,还要中函数调用说起。

静态的函数只是一堆二进制指令,函数要想运行还需要栈。栈用来存储函数的参数和局部变量(有时包括返回值),一个函数在运行时其参数和局部变量等占据的空间称为栈帧,如下图所示。

栈帧是一个函数一次运行的快照,是函数的内部状态。也就是说,函数每被调用一次,就会在栈段留下一个栈帧,即便是调用自身也会产生一个新的栈帧。如果想复用栈帧,函数就不能有内部状态,或者说它的状态不会再将来被使用。这才是尾递归的精髓,也只有这样的递归可以被优化,复用之前的栈帧,避免栈空间耗尽。如果只是把尾递归理解为在函数末尾调用自身是片面的。

首先来看下面这个列表求和的例子。

func sumSlice1(s []int) int {
  if len(s) == 0 {
    return 0
  }
  if len(s) == 1 {
    return s[0]
  }
  return s[0] + sumSlice1(s[1:])
}

func sumSlice2(s []int, tmp int) int {
  if len(s) == 0 {
    return tmp
  }
  return sumSlice2(s[1:], tmp+s[0])
}

上例中虽然sumSlice1也是在末尾调用自身,但是它不能进行尾递归优化,因为栈帧需要保存s[0]这个状态。而sumSlice2没有内部状态,可以进行尾递归优化。

还有一个典中典的递归例子是求斐波拉契数列的第n项,如下:

func Fibolacci1(n int) int {
  if n < 2 {
    return n
  }
  return Fibolacci1(n-1) + Fibolacci1(n-2)
}

func Fibolacci2(n, a, b int) int {
  if n == 0 {
    return a
  }
  return Fibolacci2(n-1, b, a+b)
}

Fibolacci1看似没有内部状态,但其实Fibolacci1(n-1)本身就是一个需要存储的状态,我们可以改写如下:

func Fibolacci1(n int) int {
  if n < 2 {
    return n
  }
  a := Fibolacci1(n-1)
  b := Fibolacci1(n-2)
  return a + b
}

这样就能明显看出它不是尾递归了。Fibolacci2没有内部状态,是可以做尾递归优化的版本。

说到状态,我们知道编程语言有两大阵营:函数式编程和命令式编程。前者努力消除状态,后者依赖于状态。所以一般只有函数式编程语言的编译器会去做尾递归优化,然而并不是所有的递归都是天然的尾递归,许多时候需要我们手动消除状态,方式就是把局部变量(状态)变成参数,我称之为状态参数化。这也是函数式编程常用的手段。

再多思考一点,如果将函数调用看作一条时间线,先执行的函数是过去,被调用的函数是未来,也就是过去需要用到将来的结果,这显然是不可能的,至少目前我们的物理学还是因果的。既然无法把将来带回到过去,那么就只能把过去带去未来,方法就是把它记录下来。记忆是人类大脑一项重要的功能,我们也是通过记忆把过去的信息带到未来。

注意,尾递归优化需要编译器的支持,我们在这里讨论的是尾递归的本质以及如何把编译器不能优化的递归改写成可以优化的递归。虽然我们的例子都是用Go写的,但是目前Go编译器是不支持尾递归优化的,想体验尾递归优化可以试试Erlang。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/351614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

共享模型之工具(二)

1.自定义线程池 1>.在实际开发过程中建议不要使用JDK提供的方式创建线程池,因为底层不方便优化,在请求量非常大的情况下可能会出现OOM,我们需要手动实现一个线程池; 2>.代码实现: Slf4j public class TestThreadPoolDemo1 {public static void main(String[] args) {/…

容器安全风险and容器逃逸漏洞实践

本文博客地址&#xff1a;https://security.blog.csdn.net/article/details/128966455 一、Docker存在的安全风险 1.1、Docker镜像存在的风险 不安全的第三方组件&#xff1a;用户自己的代码依赖若干开源组件&#xff0c;这些开源组件本身又有着复杂的依赖树&#xff0c;甚至…

在 Python 中只接受数字作为用户输入

只接受数字作为用户输入&#xff1a; 使用 while True 循环进行循环&#xff0c;直到用户输入一个数字。使用 float() 类尝试将值转换为浮点数。如果用户输入了一个数字&#xff0c;请使用 break 语句跳出循环。 while True:try:# &#x1f447;️ use int() instead of floa…

宝马项目化流程标准(BMW ABC flyer requirement)

ABC flyer/ BMWQMT build phase requirement宝马的项目流程标准叫做ABC flyer,也叫QMT build phase requirement.为什么叫这么名字&#xff0c;是因为宝马项目的产品零件分为几个阶段&#xff1a;A-samples, B-samples,C-samples, initial-samples.1、PVL/ VS0:100% ok parts i…

高通平台开发系列讲解(Android篇)AudioTrack音频流数据传输

文章目录 一、音频流数据传输通道创建1.1、流程描述1.2、流程图解二、音频数据传输2.1、流程描述2.2、流程图解沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要图解AudioTrack音频流数据传输 。 一、音频流数据传输通道创建 1.1、流程描述 AudioTrack在set函…

项目自动化构建工具make/Makefile

目录 make/Makefile概念和关系 make/Makefie的使用 一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的规则来指定&#xff0c;哪些文件需要先编译&#xff0c;哪些文件需要后编译&#xff0c;哪些文件需要重…

33岁测试宝妈,在家带娃一年半,入职新公司居然年薪30W+

疫情3年&#xff0c;每一个行业的危机&#xff0c;每一个企业的倒下&#xff0c;背后都是无数人的降薪、降职和失业。这也暴露了人生的残酷真相&#xff1a;人活一辈子&#xff0c;总有“丰年”和“荒年” 优秀的测试既过得了丰年&#xff0c;也受得住荒年。 我之前认识的一个…

零信任-Zscaler零信任介绍(7)

​Zscaler零信任介绍 Zscaler是一家专注于网络安全的公司&#xff0c;他们提供了一种名为Zscaler Zero Trust Exchange (ZTX)的零信任解决方案。这种解决方案旨在帮助企业提高网络安全&#xff0c;并确保只有授权的用户&#xff0c;设备和应用程序才能访问敏感信息。ZTX采用多…

畅购电商项目

1. 电商项目架构图技术框架/技术选型1.1 系统架构该项目是一个B2C的电商项目&#xff08;类似小米商城、京东商城、天猫商城&#xff09;允许客户通过网络购买商品该项目主要完成的是电商项目前台的开发。采用前后端分离的方式进行开发的前端&#xff1a;vue全家桶&#xff08;…

QT入门Containers之Widget、Frame

目录 一、QWidget界面相关 1、布局介绍 2、基本界面属性 3、特殊属性 二、QFrame 三、Demo展示 此文为作者原创&#xff0c;创作不易&#xff0c;转载请标明出处&#xff01; 一、QWidget界面相关 1、布局介绍 为什么将QWidget容器放在第一个&#xff0c;因为目前使用过…

前端缓存知识-强缓存与协商缓存

缓存的作用 减少了冗余的数据传输&#xff0c;节省了网费。减少了服务器的负担&#xff0c; 大大提高了网站的性能加快了客户端加载网页的速度 缓存分类 强制缓存如果生效&#xff0c;不需要再和服务器发生交互&#xff0c;而对比缓存不管是否生效&#xff0c;都需要与服务端…

查询蓝牙适配器版本

台式机不支持蓝牙&#xff0c;装了个蓝牙适配器&#xff0c;结果换来换去又忘记自己这个是啥版本了&#xff0c;适配器自己也不写。好在微软官方也给了说明如何查询我的电脑运行哪个蓝牙版本&#xff1f; - Microsoft 支持https://support.microsoft.com/zh-cn/windows/%E6%88%…

day44【代码随想录】动态规划之零钱兑换II、组合总和 Ⅳ、零钱兑换

文章目录前言一、零钱兑换II&#xff08;力扣518&#xff09;二、组合总和 Ⅳ&#xff08;力扣377&#xff09;三、零钱兑换&#xff08;力扣322&#xff09;总结前言 1、零钱兑换II 2、组合总和 Ⅳ 3、零钱兑换 一、零钱兑换II&#xff08;力扣518&#xff09; 给你一个整数…

教你如何实现一个页面自动打字效果

前言&#xff1a; 最近在写一个类似于 windows 启动页的项目&#xff0c;不知道大家是否还记的 windows 很经典的小光标加载页面&#xff0c;我又稍微改造了一下效果如下&#xff1a; 一. 光标闪烁效果的实现 tips&#xff1a; 在这里我们使用了 UnoCSS&#xff0c;如果你不清…

金三银四,如果没准备这些面试题,找工作还是缓一缓吧

前言: 最近金三银四跳槽季&#xff0c;相信很多小伙伴都在面试找工作&#xff0c;怎样才能拿到大厂的offer&#xff0c;没有掌握绝对的技术&#xff0c;那么就要不断的学习… 如何拿下阿里等大厂的offer的呢&#xff0c;今天分享一个秘密武器&#xff0c;资深测试开发师整理的…

【面试问题-java内存模型JMM】

今天面试&#xff0c;我把运行时数据区域答成了java内存模型&#xff0c;回来把这方面的问题给纠正一下。 以下内容阅读自《深入理解Java虚拟机》第12章 下面小段只做了解即可。重点是Java内存模型。 多任务处理在现代计算机操作系统中是必备的功能。 计算机运行速度与它的存储…

【MySQL】数据库基础

目录 1、什么是数据库 2、 数据库基本操作 2.1 查看当前数据库 2.2 创建一个数据库 2.3 选中数据库 2.4 删除数据库 3、常见的数据类型 3.1 数值类型 3.2 字符串类型 3.3 日期类型 4、表的操作 4.1 创建表 4.2 查看指定数据库下的所有表 4.3 查看表的结构 4.…

java常见的异常

异常分类 Throwable 是java异常的顶级类&#xff0c;所有异常都继承于这个类。 Error,Exception是异常类的两个大分类。 Error Error是非程序异常&#xff0c;即程序不能捕获的异常&#xff0c;一般是编译或者系统性的错误&#xff0c;如OutOfMemorry内存溢出异常等。 Exc…

环境变量和进程地址空间

目录 环境变量&#xff1a; env&#xff1a;显示所有的环境变量&#xff1a; echo $环境变量名表示查看环境变量的值 理解环境变量&#xff1a; getenv&#xff1a;显示环境变量的值 export set命令&#xff1a;显示所有变量 unset取消变量&#xff1a; pwd&#xff1a;当…

Django框架之模型查询-关联查询

关联查询 查询书籍为1的所有人物信息 查询人物为1的书籍信息由一到多的访问语法&#xff1a; 一对应的模型类对象.多对应的模型类名小写_set 例&#xff1a; >>> book BookInfo.objects.get(id1) >>> book.peopleinfo_set.all() <QuerySet [<Peopl…