Ruby 浅谈尾递归

ruby脚本递归处理指定路径下的文件 该脚本可以扫描指定路径,将符合条件的文件全部找出。你可以添加自己的函数来处理符合条件的文件。如删除某个文件夹里的所有特定文件 立即下载

来源:https://ruby-china.org/topics/36896

初入豆厂的时候就经常听到一些有经验的老员工谈到尾递归,当时我也不怎么当回事。相信许多初入职场的同学也跟我一样对于一些与自己所做工作似乎没有直接关系的东西一般都持排斥的态度。现在回想起来真想扇自己两巴掌,如果当时能够好好的了解一下这些概念的话,或许我能更早地发现编程里面更深层次的乐趣。

Ruby Title

直到最近翻阅《SICP》这本书,书中再次提到尾递归这个概念,我知道自己逃不掉了,这次一定要把它弄清楚。

1. 递归

递归应该是我们耳熟能详的一个概念了,通常在一些涉及高级计算机的理论的书籍或者课程中都会涉及这个话题。但是,在工作中能够直接运用上的机会并不是很多,这也使得它在我们心目中的位置被神化了。递归其实就是一个函数在调用自身的过程。为了更直观地了解这个概念,我们从一道面试题开始

请你实现一个计算阶乘的函数(只能够接受整数输入)

相信很多朋友都能够信手拈来,下面是Ruby版本的实现

def factorial(n)
  return 1 if n <= 1
  n * factorial(n - 1)
end

运行起来也是比较符合预期的

> factorial(2)
 => 2
> factorial(10)
 => 3628800
> factorial(30)
 => 265252859812191058636308480000000

但是当我们用这个阶乘函数来换算一个较大的数的时候,就会导致栈溢出的错误了

> factorial(100000)
SystemStackError: stack level too deep
    from (irb):3:in `factorial'
    from (irb):3:in `factorial'
    from (irb):3:in `factorial'
  .....

Why?我们来简单地分析一下上面的过程。递归确实可以使我们的代码更优雅,但是优雅的背后是要付出代价的。使用递归需要记录函数的调用栈,当调用栈太深的话则将造成栈溢出问题。下面以factorial(6)为例来展示这个阶乘函数的计算过程

factorial(6)
6 * factorial(5)
6 * (5 * factorial(4))
6 * (5 * (4 * factorial(3)))
6 *(5 * (4 * (3 * factorial(2))))
6 *(5 * (4 * (3 * (2 * factorial(1)))))
##############################
6 * (5 * (4 * (3 * (2 * 1))))
6 * (5 * (4 * (3 * 2)))
6 * (5 * (4 * 6))
6 * (5 * 24)
6 * 120
720

当n等于6的时候,我们调用栈的深度就为6。分割线以上的部分是展开的过程,也可以理解成是调用栈堆积的过程,而分割线以下的过程是换算过程, 也可以理解为释放调用栈的过程。可以预测调用栈随着我们传入的参数n的增长而呈线性增长。回到栈溢出的那条程序,当我们n等于100000的时候,调用栈的深度超出了预设的阀值,就会导致了Ruby报栈溢出的错误。为了解决这种递归过程中调用栈过深而引发的内存问题,我们可以借助尾递归。

2. 尾递归

上面的计算过程被称为递归计算过程,计算过程中构造了一个推迟进行的操作所形成的链条(分割线以上),收缩阶段表现为运算实际的执行(分割线以下)。而尾递归则是一种迭代计算过程。

1) 迭代计算

迭代的概念我们接触得比较多了,一般的编程语言都会有迭代的关键字,比如for,each,while等等。在介绍尾递归之前我先用迭代的方式来实现一个阶乘的计算过程,下面是Ruby的实现,为了更直观,我先用一个外部变量来缓存每一次乘积的结果

a = 1

(1..6).each do |i|
  a = a * i
end

puts a

计算结果是一样的,6的阶乘等于720。借鉴这种方式如果我们能够在递归的过程中用一个类似a的变量存储计算过程的中间结果,而不是在原有的栈基础上进行叠加,弄成了一个长长的调用栈来延迟执行,那样岂不是能够剩下许多栈的资源?

2) 尾递归版本

我们可以尝试在递归的过程中维护一个变量来存储计算过程的中间结果,每次递归的时候把这个结果传送到下一次计算过程,达到特定条件的时候终止程序。在具体分析这个过程之前我先贴出代码

# 用a来存储计算的中间结果,并将结果作为下一次递归的参数
def fact_iter(i, n, a)
  a = i * a

  return a if i >= n

  i += 1
  fact_iter(i, n, a)
end

def factorial(n)
  fact_iter(1, n, 1)
end

上面的代码并没有最初的递归版本那么优雅了,我另外定义了一个方法fact_iter(i, n, a),来分别接收索引, 最大值, 累乘值这三个参数。下面来看一下如今的调用栈又是如何呢?

factorial(6, 1)
fact_iter(1, 1, 6)
fact_iter(1, 2, 6)
fact_iter(2, 3, 6)
fact_iter(6, 4, 6)
fact_iter(24, 5, 6)
fact_iter(120, 6, 6)
fact_iter(720, 7, 6)

可见上面的过程并没有像递归计算过程那样还有一个长长的调用栈,它的调用过程更加平滑,《SICP》把这个过程的总结为

它总能在常量空间中执行迭代型的计算过程,即使这一计算过程是用一个递归过程描述的,具有这一特征的实现称之为尾递归。

OK,具有尾递归特性的代码已经实现了。现在测试一下,它相比于前面的递归版本是否能帮我们节省一些计算资源,避免掉栈溢出的情况。

> factorial(30)
=> 265252859812191058636308480000000

> factorial(100000)
SystemStackError: stack level too deep
    from (irb):18:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'
    from (irb):23:in `fact_iter'

What? 显然花了那么多时间实现的尾递归版本并没能带来什么实质性的效果,栈依然溢出了。不过这是Ruby的问题,它默认没有开启尾递归优化,毕竟每门语言都有它自己的特性,不然如果每门语言都一样的话岂不是少了许多乐趣?接下来我会简单讲讲如何在Ruby里面启动尾递归优化。

3. Ruby不支持尾递归优化吗?

有些人说Ruby不支持尾递归优化这个说法并不是十分准确,应该描述成Ruby默认没有开启尾递归优化这个选项。大家都知道在Ruby1.9之后就有了虚拟机这个概念了,在这个版本之后Ruby代码都会先编译成字节码,然后把字节码放到虚拟机上面运行。我们可以修改虚拟机的编译选项来启动尾递归优化,相关的配置选项,如下

> require 'pp'
> pp RubyVM::InstructionSequence.compile_option
{:inline_const_cache=>true,
 :peephole_optimization=>true,
 :tailcall_optimization=>false,
 :specialized_instruction=>true,
 :operands_unification=>true,
 :instructions_unification=>false,
 :stack_caching=>false,
 :trace_instruction=>true,
 :frozen_string_literal=>false,
 :debug_frozen_string_literal=>false,
 :debug_level=>0}

可见tailcall_optimization这个尾递归相关的优化选项默认是false的,另外还有一个跟踪指令的选项trace_instruction这个默认是true,我们只需要开启前者,关闭后者就可以启动尾递归优化了。我把配置代码与方法定义的代码分别写到两个Ruby的脚本文件中

## config.rb
RubyVM::InstructionSequence.compile_option = {tailcall_optimization: true, trace_instruction: false}
# factorial.rb
def fact_iter(i, n, a)
  a = i * a

  return a if i >= n

  i += 1
  fact_iter(i, n, a)
end

def factorial(n)
  fact_iter(1, n, 1)
end

在REPL环境中分别加载这两个脚本,注意一定要先加载配置文件,然后再定义方法,如果把两个东西都放在同一个脚本里面,Ruby解析器会同时编译,导致方法定义的时候无法应用到最新的配置。

> require "./config.rb"
=> true
> require "./factorial.rb"
=> true

OK, 这次我们再来计算一次100000的阶乘的话就不会再出现栈溢出的情况了,但是计算出来的数字很大,我只能贴出其中一小部分了

> factorial(100000)

282422940796034787429342157802453551847749492609122485057891808654297795090106301787255177141383116361071361173736196295147499618312391802272607340909383242200555696886678403803773794449612683801478751119669063860449261445381113700901607668664054071705659522612980419........

4. 尾声

这篇文章首先用递归的方式来实现阶乘函数,但是我们发现在计算较大的数的时候就会有栈溢出的现象。这个时候我们可以采用尾递归来优化我们原来的阶乘函数,使之能够在常量计算空间内完成整个递归过程。尾递归并不是某些语言的专属,许多语言都可以写出尾递归的代码(Ruby, Python等等),但这些语言里面并不是所有都能够支持尾递归优化,Ruby默认就没有开启尾递归优化,为此我在最后简单地讲了下在Ruby里面如何修改配置启动尾递归优化,让我们的尾递归代码能够生效。

参考文档

Happy Coding and Writing!!

DCM、PLL以及DLL等概念及详情 介绍DLL、PLL以及DCM的概念,总结其模块构成、工作原理、相互之间的差异与各自优势。 阅读详情

相关推荐

【PyTorch版本兼容性指南】如何在不同版本中正确使用torch.nn.attention

本文详细解析了PyTorch版本兼容性问题,特别是torch.nn.attention模块在不同版本中的使用差异。针对常见的ModuleNotFoundError错误,提供了三种实用解决方案:升级PyTorch版本、使用自定义实现和源码移植方案,并分享了最佳实践建议,帮助开发者有效避免兼容性问题。

weixin_29084231的博客 472

递归函数的递归出口_Ruby:使用函数,递归,范围和块的技巧

递归函数的递归出口 背景 (Background) I’ve been studying for technical interviews: doing a bunch of problems on Leetcode, and anytime I needed to use recursion for DFS or any other problems, I got really confused...

weixin_26737625的博客 1075

SAP公司间采购后台配置及前台操作详解

公司间采购后台配置及前台操作详解 SAP 实施中公司间交易几乎每个项目都会有此业务需求,比如内地很多公司都会在香港设置贸易型子公司。通过香港公司接单,然后转内地公司生产,或内地公司通过香港公司采购进口原材料,这些业务必然会涉及到公司间交易,下面我将详细说明下SAP是如何实现这种业务场景的,供大家参考。 配置前必须前台先创建公司间供应商主数据和客户主数据 1. 创建相应的客户主数据 功能说明 公司间...

weixin_40672823的博客 1万+

浅谈尾递归

初入豆厂的时候就经常听到一些有经验的老员工谈到尾递归,当时我也不怎么当回事。相信许多初入职场的同学也跟我一样对于一些与自己所做工作似乎没有直接关系的东西一般都持排斥的态度。现在回想起来真想扇自己两巴掌,如果当时能够好好的了解一下这些概念的话,或许我能更早地发现编程里面更深层次的乐趣。 直到最近翻阅《SICP》这本书,书中再次提到尾递归这个概念,我知道自己逃不掉了,这次一定要把它弄清楚。 1. 递...

weixin_34348111的博客 207

ROS2_IGH方案配置(二)——Ubuntu22.04 实时内核性能验证与调优

本文详细介绍了在Ubuntu22.04系统上配置ROS2_IGH方案时,如何验证和调优PREEMPT_RT实时内核性能。通过使用cyclictest工具进行基准测试和压力测试,分析中断与硬件延迟,并优化内核参数,确保系统满足工业控制领域对硬实时性的苛刻要求。文章还提供了针对ROS2的特殊优化建议和性能监控方案,帮助开发者实现毫秒级延迟的稳定控制。

weixin_30791095的博客 224

S4-FPGA-K7板级原理图硬件实战

xilinx K7板级硬件实战

weixin_66510961的博客 6154

ruby 使用递归方法,计算n 的阶乘

递归:指在当前方法内调用自己,设置停止条件的这种现象。 递归的分类: 递归分为两种,直接递归和间接递归。 优点:使用递归编写的程序简洁、结构清晰,程序的正确性很容易证明,不需要了解递归调用的具体细节。 缺点:递归函数在调用的过程中,每一层调用都需要保存临时性变量和返回地址、传递参数,因此递归函数的执行效率低。 简单递归 求n的阶乘、斐波那契数列、求n个数的最大、数制转换、求最大公约数都属于简单递归。 计算阶乘:例如 n = 3 结果为 3 x 2 x 1 = 6 n = 5 . 结果为 5.

琳琳的博客 552

xtop:multi_driven_net与incomplete_timing_cell fail reason 分析

man multi_driven_net”可以得知,这个问题是multi driven结构引起的,使用命令“get_failed_pins -reason multi_driven_net”,获取violation pin。“man incomplete_timing_cell”可以得知,这个fail reason的原因是缺少timing library cell,检查并补全后这个错误就显示了。这里选择的解决方法是修改lib文件,把inout改成input,重新lib2idb。通过这种方法解决了问题。

m0_61544122的博客 568

尾递归是个什么鬼

了解尾递归之前,先了解一下尾调用。 在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这种情形下该调用位置为尾位置。(摘自维基百科) 以上的解释来自维基百科。介绍了什么叫尾调用。例如: function foo(data) { a(data); return b(data); } ...

weixin_30443895的博客 893

【ARM CoreLink 系列 7 -- TZC-400控制器简介】

为了确保内存能够正确识别总线的信号控制位,新增一个TrustZone地址空间控制器(TrustZone Address Space Controller,TZASC)组件来实现对内存地址的安全扩展,将内存区域划分为安全内存区域和非安全内存区域,安全世界可以访问安全内存和非安全内存,而普通世界只能访问非安全内存,如下图所示:图 1-1内存区域的划分是在系统启动时通过配置 TZASC 组件(TZC-400/TZC-380)来实现的。TZC-400由和使能情况;安全访问权限;Base地址;

CodingCos的博客 1634

风光储并网对电力系统稳定性影响的仿真研究

本文研究了风光储并网对电力系统稳定性的影响,基于MATLAB/Simulink平台搭建了IEEE 9节点测试系统的风光储并网模型。研究对比了光储、风储和风光储三种并网配置在多种干扰下的稳定性表现。结果表明:光储系统对频率变化最敏感,风储系统具有更强的功率波动适应能力,而风光储联合系统通过互补效应展现出最佳综合稳定性。储能系统在所有配置中均显著改善了频率恢复性能,验证了其在新能源并网中的关键支撑作用。研究为高比例可再生能源电力系统的稳定运行提供了仿真依据和技术参考。

huanghm88的专栏 156

递归与尾递归总结

本文转载自: https://www.cnblogs.com/alantu2018/p/8465884.html 递归与尾递归总结 1、递归   关于递归的概念,我们都不陌生。简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。用递归需要注意以下两...

weixin_34247032的博客 204

解决Todesk连接卡在100%问题:remote display server is not supported (e.g. Wayland)

摘要:解决Ubuntu 22.04远程桌面连接问题(AnyDesk报错"remote display server not supported"及向日葵/Todesk无法显示)。方法:1)安装lightdm显示管理器;2)启用lightdm并禁用gdm;3)编辑/etc/gdm3/custom.conf文件,添加WaylandEnable=false;4)重启系统。注:后续可通过命令切换回gdm显示管理器。该方法适用于解决Wayland显示协议导致的远程桌面连接兼容性问题。

猿小白的博客 1573

关于全排列的递归算法(Ruby实现)

记录一下自己关于全排列的递归算法的感悟。

振爷啊的博客 837

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

ruby 递归调用

#encoding: utf-8 puts "hello" class REC attr_reader :ids def initialize @ids = [] end def get_work_ticket(days) puts "func get work ticket" if(days>10)

stackoverflow 2336

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

ruby递归查找某对象的父/子节点集合

def parent_ids ids = [] parent_id = self.parent_id while parent_role = Role.find_by_id(parent_id) if parent_role ids

jamst 459

Ruby插入排序的常规写法和递归写法

用循环的常规写法 arr=[5,2,3,1,4]#循环下表1~arr.length,注意初始时最左边因为只有一个值,所以可以看成左边数组0~1已经有序 1.upto arr.length-1 do |outIndex| #取值 element=arr[outIndex] #生成子下标,数值为外部下标的左边一位(因为自己与自己比较没有意义) index=outIndex-1 #下标

o83290102o5的博客 404

递归函数详解

递归函数就是在函数体内部调用自己的函数众所周知,递归思维在编程界影响深远,一重递归简单明了,很容易看出执行顺序,但是递归有了嵌套后,你是否还能理解其执行过程呢?1.单重递归。#include&lt;iostream&gt;#include&lt;string&gt;using namespace std;int m = 5,n = 5;void view(string sign,int value...

愿你饱经冷暖,仍保纯真 1万+

递归及尾递归优化

1、递归介绍 递归简而言之就是自己调用自己。使用递归解决问题的核心就是分析出递归的模型,看这个问题能拆分出和自己类似的问题并且有一个递归出口。比如最简单的就5的阶乘,可以把它拆分成5*4!,然后求4!又可以调用自己,这种问题显然可以用递归解决,递归的出口就是求1!,可以直接返回1。用Python实现如下: def fact(n): if n==1: return n return n...

上善若水 7367

尾递归:实际中为什么快排会比堆排快?

尾递归 尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去. 递归解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统为每一层的返回点

linkedin_39403967的博客 2004

递归简单总结

什么是递归? 递归就是程序不断的调用自身。 递归分为直接递归和间接递归: •直接递归:函数在执行过程中调用本身。 •间接递归:函数在执行过程中调用其它函数再经过这些函数调用本身。 递归四大要素: 1.递归要有出口,也就是终止点,否则会一直循环,知道资源耗尽 2.问题不断缩小 3.问题通过再次递归可得到解决 4.子问题的组的解应该问题的最终解 例子: 将一个8两重的苹果分

withinheart的博客 2218

递归(Recursion)的两种优化方法

递归(Recursion)的两种优化方法最近学习了一些递归的优化方法,于是想写一篇文章记录一下,方便以后查看,并想分享一下。 虽然平日的上课中老师都是建议我们不要使用递归,但是在函数式编程(functional programming)中递归是很常用的,而且掌握递归的思想有时能让我们找到最优解。1. 最经典的斐波那契数列int Fibonacci(int n) { if (n == 1 |

dccif的博客 1万+

尾递归,递归优化

一、什么是尾递归 程序调用自身的编程技巧称为递归( recursion)。尾递归是一种特殊的递归,递归形式的调用都出现在函数的末尾,我们称这个 递归函数是尾递归的。 二、尾递归的优势 尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代 码。这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈...

petershuang的博客 1117

什么是尾递归,尾递归的优势以及语言支持情况说明

今天在进行数据排序时候用到递归,但是耗费内存太大,于是想找一找有没有既提升效率又节省内存的算法,然后发现尾递归确实不错,只可惜php并没有对此作优化支持. 虽然如此,但还是学习了,下面总结一下: 尾递归 --概念 如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是

houzhyan-博客 5389
上一篇: 【语法篇】14、浅谈递归函数
下一篇: 普通递归与尾递归求解n!
Estrellas_
博客等级 码龄9年 273粉丝 233原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值