递归比循环更快吗?

我知道递归有时比循环更清洁,而且我不会问什么时候应该使用递归迭代,我知道还有很多问题。

我问是什么,是有史以来递归比一个循环更快? 对我来说,似乎总是可以改进一个循环,并使其比递归函数更快地执行,因为循环不会不断设置新的堆栈帧。

我特别关注递归是否是处理数据的正确方法,比如在二叉树中的某些排序函数中,递归是否更快。


这取决于使用的语言。 你写了'语言不可知论',所以我会举一些例子。

在Java,C和Python中,与迭代(通常)相比,递归相当昂贵,因为它需要分配一个新的堆栈帧。 在一些C编译器中,可以使用编译器标志来消除这种开销,它将某些类型的递归(实际上是某些类型的尾部调用)转换为跳转而不是函数调用。

在函数式编程语言实现中,有时迭代可能非常昂贵,而且递归可能非常便宜。 在很多情况下,递归转化为简单的跳转,但是改变循环变量(这是可变的)有时需要一些相对较大的操作,尤其是在支持多线程执行的实现上。 由于mutator和垃圾收集器之间的交互作用(如果两者可能同时运行),在这些环境中的一些环境中变异是昂贵的。

我知道在一些Scheme实现中,递归通常会比循环更快。

简而言之,答案取决于代码和实现。 使用你喜欢的任何风格。 如果您使用的是功能语言,递归可能会更快。 如果你使用命令式语言,迭代速度可能会更快。 在某些环境中,两种方法都会导致生成相同的组件(将其放入管道中并冒烟)。

附录:在某些环境中,最好的选择是既不是递归也不是迭代,而是高阶函数。 这些包括“地图”,“过滤器”和“减少”(也被称为“折叠”)。 这不仅是首选的风格,它们不仅更清晰,而且在某些环境中,这些函数是第一个(或者仅仅)能够从自动并行化中获得提升 - 因此它们可以比迭代或递归快得多。 数据并行Haskell就是这种环境的一个例子。

列表解析是另一种选择,但这些通常只是用于迭代,递归或更高阶函数的语法糖。


递归比循环更快吗?

不,迭代总是比递归更快。 (在冯诺依曼体系结构中)

说明:

如果从零开始构建通用计算机的最小操作,则“迭代”首先作为构建块,并且比“递归”的资源密集度要低,但ergo速度更快。

从头构建一个伪计算机器:

自问 :你需要计算一个值,即遵循一个算法并达到一个结果?

我们将建立一个概念层次结构,从头开始,首先定义基本的核心概念,然后用这些概念构建二级概念,等等。

  • 第一个概念: 内存单元,存储,状态 。 要做某些事情,您需要存储最终和中间结果值的位置。 我们假设我们有一个无限数组的“整数”单元,称为存储器 ,M [0..Infinite]。

  • 说明:做点事 - 改造一个细胞,改变它的价值。 改变状态 。 每个有趣的指令都会进行转换。 基本说明是:

    a) 设置并移动内存单元

  • 将值存储到内存中,例如: store 5 m [4]
  • 将一个值复制到另一个位置:例如: store m [4] m [8]
  • b) 逻辑和算术

  • 和,或者,异或,不
  • 加,分,多,分。 例如添加m [7] m [8]
  • 执行代理 :现代CPU中的核心。 “代理”是可以执行指令的东西。 代理也可以是一个人在纸上遵循算法。

  • 步骤顺序:一系列指令 :即:先执行此操作,然后执行此操作,等等。一系列必要的指令。 即使是一行表达式也是“一个必要的指令序列”。 如果你有一个特定的“评估顺序”表达式,那么你有步骤 。 这意味着即使单个组合表达式具有隐含的“步骤”,并且也具有隐式局部变量(我们称之为“结果”)。 例如:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    上面的表达意味着隐含“结果”变量的3个步骤。

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    所以,即使中缀表达式,因为你有一个特定的评估顺序,所以它是一系列必要的指令。 表达式意味着要按特定顺序进行的一系列操作,并且由于有步骤,所以还有一个隐含的“结果”中间变量。

  • 指令指针 :如果你有一系列步骤,你也有一个隐含的“指令指针”。 指令指针标记下一条指令,并在指令读取之后但执行指令之前前进。

    在这个伪计算机中,指令指针是存储器的一部分。 (注意:通常指令指针是CPU核心中的“特殊寄存器”,但这里我们将简化概念并假定所有数据(包含寄存器)都是“存储器”的一部分)

  • 跳转 - 一旦你有一个有序的步数和一个指令指针,你可以应用“ 存储 ”指令来改变指令指针本身的值。 我们将用一个新的名称来调用这个存储指令的具体用法: 跳转 。 我们使用一个新的名字,因为它更容易被认为是一个新概念。 通过更改指令指针,我们正在指示代理“转到步骤x”。

  • 无限迭代 :通过跳回,现在可以让代理“重复”一定数量的步骤。 此时我们有无限的迭代。

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  • 有条件的 - 有条件地执行指令。 使用“条件”子句,可以根据当前状态(可以使用前一条指令设置)有条件地执行几条指令之一。

  • 适当的迭代 :现在使用条件子句,我们可以避开跳回指令的无限循环。 我们现在有一个条件循环 ,然后进行适当的迭代

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  • 命名 :将名称命名为保存数据或保存步骤的特定内存位置。 这只是一种“便利”。 我们不会添加任何新的说明,因为有能力为内存位置定义“名称”。 “命名”不是代理人的指令,这对我们来说只是一种方便。 命名使代码(此时)更易于阅读并且更易于更改。

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  • 一级子程序 :假设您需要频繁执行一系列步骤。 您可以将步骤存储在内存中的指定位置,然后在需要执行它们(调用)时跳转到该位置。 在序列结束时,您需要返回到调用点以继续执行。 有了这个机制,你就可以通过编写核心指令来创建新的指令(子程序)。

    实施:(不需要新的概念)

  • 将当前指令指针存储在预定义的内存位置
  • 跳转到子例程
  • 在子程序结束时,您从预定义的内存位置检索指令指针,从而有效地跳回到原始调用的以下指令
  • 问题的一个层次的实现:你不能从一个子程序调用另一个子程序。 如果你这样做,你会覆盖返回的地址(全局变量),所以你不能嵌套调用。

    更好地实现子例程:您需要一个STACK

  • 堆栈 :你可以定义一个内存空间作为一个“堆栈”,你可以在堆栈上“推送”值,也可以“弹出”最后一个“推送”的值。 要实现一个堆栈,你需要一个堆栈指针 (类似于指令指针),它指向堆栈的实际“头部”。 当你“推”一个值时,堆栈指针递减并且存储该值。 当你“弹出”时,你得到实际堆栈指针的值,然后堆栈指针递增。

  • 子程序现在我们有了一个堆栈,我们可以实现允许嵌套调用的正确的子程序。 这个实现类似,但不是将指令指针存储在预定义的存储位置,而是将IP的值“推”到堆栈中。 在子程序结束时,我们只是“弹出”栈中的值,在原始调用之后有效地跳回到指令。 这个具有“堆栈”的实现允许从另一个子例程调用一个子例程。 通过这个实现,我们可以通过使用核心指令或其他子例程作为构建块,将新指令定义为子例程时创建多个抽象级别。

  • 递归 :子程序自行调用时会发生什么? 这被称为“递归”。

    问题:覆盖本地中间结果,子程序可以存储在内存中。 由于您正在调用/重复使用相同的步骤, 如果中间结果存储在预定义的内存位置(全局变量)中,它们将在嵌套调用中被覆盖。

    解决方案:为了允许递归,子例程应该将本地中间结果存储在堆栈中 ,因此,在每次递归调用(直接或间接)时,中间结果都存储在不同的存储器位置。

  • ...

    已经达到递归我们停在这里。

    结论:

    在冯·诺伊曼体系结构中,显然“迭代”是比“递归”更简单/基本的概念。我们在第7层有一种“迭代”形式,而“递归”在概念层次结构的第14层。

    机器代码中的迭代总是会更快,因为它意味着更少的指令,因此CPU周期更少。

    哪一个更好”?

  • 当你处理简单的,顺序的数据结构时,你应该使用“迭代”,并且在“简单循环”的任何地方都应该使用“迭代”。

  • 当需要处理递归数据结构(我喜欢称它们为“分形数据结构”)时,或者当递归解决方案明显更“优雅”时,应该使用“递归”。

  • 建议 :为工作使用最好的工具,但理解每个工具的内部工作以明智地选择。

    最后,请注意,您有很多使用递归的机会。 你在任何地方都有递归数据结构,你现在正在看一个:支持你读的是DOM的部分是RDS,JSON表达式是RDS,计算机中的分层文件系统是RDS,即:你有包含文件和目录的根目录,包含文件和目录的每个目录,包含文件和目录的每个目录......


    在替代方法是显式管理堆栈时递归可能会更快,就像您提到的排序或二叉树算法一样。

    我有一个在Java中重写递归算法的例子使得它更慢。

    所以正确的做法是首先以最自然的方式写出来,只有在分析显示它很重要时才进行优化,然后衡量所期望的改进。

    链接地址: http://www.djcxy.com/p/14169.html

    上一篇: Is recursion ever faster than looping?

    下一篇: python 3.x