`

递归与尾递归

阅读更多

       一句话理解递归与尾递归

      递归就是普通涵数的调用,消耗资源

      尾递归就是最后一个调用自已,中间不需要处理数据,所以资源消耗层面很少。

      这就象迭代器的好处。

       编程很复杂,编程也很简单。简单的逻辑,通过代码组织,就可以变成复杂程序或者系统。以前学物理的时候,老师就说考试的物理题其过程是相当复杂的(简单的就没有必要考了)。解题方法众多,分解法即是一个行之有效的方式。复杂的过程经过分解,会变成简单的定理。如同螺丝,轮胎,玻璃都很简单,却能组合而成复杂的汽车。

编程也类似,核心哲学甚至简单得令人发指,其一是指针,其二是递归。深入理解者两个概念,很多复杂的系统或者设计,都会化繁为简,一目了然。

递归

递归,一个函数在内部调用自己,就是递归。递归在生活中也很常见,例如我们的眼睛,你看对方的眼睛,对方的眼睛里面有你,而那里面那个你又有她,无限循环。再比如,当你拿着一面镜子,对着另外一面镜子的时候,就会发现镜子之中有你手指的镜子,等等。

12903486_211n.jpg
12903486_211n.jpg

尾递归

函数中可以调用自己成为递归,也可以在末尾调用别的函数。如果一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这样的调用为尾调用。如果是尾调用自己,即为尾递归

尾递归是一种形式, 这种形式表达出的概念可以被某些编译器优化. 尾递归的特殊形式决定了这种递归代码在执行过程中是可以不需要回溯的(通常的递归都是需要回溯的). 如果编译器针对尾递归形式的递归代码作了这种优化, 就可能把原本需要线性复杂度栈内存空间的执行过程用常数复杂度的空间完成.

尾递归通常用于实现以下重复的计算。而一般的语言却不支持尾递归,也就是并没有被优化。例如java, python。它们使用循环迭代来达到同样的效果。

阶乘计算

解释递归最常用的例子就是阶乘算法,下面使用 PythonElixirScheme分别实现常用的递归算法。

class Factorial(object):
    @classmethod
    def recursion(cls, n):
        if n == 1:
            return 1
        return n * cls.recursion(n - 1)

Factorial.recursion(5)  # 输出 120

魔法书(SICP)中简单的演示了这个调用过程:

recursion(5)
5 * recursion(4)
5 * (4 * recursion(3))
5 * (4 * (3 * recursion(2)))
5 * (4 * (3 * (2 * recursion(1))))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120

函数调用之后,会继续调用自身,并在栈里堆积内存。scheme的解法也很简单:

#lang scheme

(define (recursion n)
  (if (= n 1)
      1
      (* n (recursion (- n 1)))))

(recursion 5) ; 输出 120

同样,elixir也很容易实现:

defmodule Factorial do
    def recursion(n) when n == 1 do
        1
    end

    def recursion(n) do
        n * recursion(n-1)
    end
end

IO.puts Factorial.recursion(5) # 输出 120

上述是递归调用,并不是尾递归,如果使用尾递归,python代码如下:

class Factorial(object):

    @classmethod
    def tail_recursion(cls, n, acc=1):
        if n == 1:
            return acc
        return cls.tail_recursion(n - 1, n * acc)

Factorial.tail_recursion(5)

尾递归的调用过程大致是

tail_recursion(5, 1)
tail_recursion(4, 20)
tail_recursion(3, 60)
tail_recursion(2, 120)
tail_recursion(1, 120)
120

编译器会根据尾递归的方式,进行优化,使得递归调用的时候并不会向线性递归那样堆积内存。就和循环迭代的效果一样。这样也是函数式编程语言处理迭代的问题。

尾递归优化主要是对栈内存空间的优化, 这个优化是O(n)到O(1)的; 至于时间的优化, 其实是由于对空间的优化导致内存分配的工作减少所产生的, 是一个常数优化, 不会带来质的变化。

那么看看scheme的实现方式

(define (tail-recursion n acc)
  (if (= n 1)
      acc
      (tail-recursion (- n 1) (* n acc))))

(tail-recursion 5 1)

看了两个例子,尾递归还是很好理解。形式上盘的就是最后一个return的时候,是单纯的返回一个函数调用,还是返回函数计算。即

  • 尾递归返回 return cls.tail_recursion(n - 1, n * acc) 只返回纯函数
  • 普通递归返回 return n * cls.recursion(n - 1) 返回函数和别的表达式运算

函数式语言基本上都支持尾递归,用来做迭代功能,下面是elixir的例子

defmodule Factorial do
    def tail_recursion(n, acc) when n == 1 do
        acc
    end

    def tail_recursion(n, acc \\ 1) do
        tail_recursion(n-1, n * acc)
    end 
end

IO.puts Factorial.tail_recursion(5)

迭代与递归

函数式编程语言,通常没有其他语言所谓的循环关键字。需要迭代的时候,可以用递归实现。最初也比较难理解递归如何实现?实际上,处理循环的时候,都是通过循环因子控制循环条件,在循环体内进行处理计算。递归也可以这样做,递归的条件终止的条件可以用递归的参数设置。

下面演示给一个列表,遍历每一个列表的元素,并给每个元素的值翻倍。同样使用三种语言代表:

class Double(object):
    @classmethod
    def recursion(cls, lst):
        if not lst:
            return []
        else:
            head, tail = lst.pop(0), lst
            return [2 * head] + cls.recursion(lst)

    @classmethod
    def tail_recursion(cls, lst, new_lst=[]):
        if not lst:
            return new_lst
        else:
            head, tail = lst.pop(0), lst
            new_lst.append(2 * head)
            return cls.tail_recursion(tail, new_lst)


Double.recursion([1, 2, 3, 4, 5])
Double.tail_recursion([1, 2, 3, 4, 5])

Scheme

(define (recursion lst)
  (if (null? lst)
      `()
      (cons (* 2 (car lst)) (recursion (cdr lst)))))


(define (tail-recursion lst new-lst)
  (if (null? lst)
      new-lst
      (tail-recursion (cdr lst) (append new-lst (list (* 2 (car lst)))))))

(recursion (list 1 2 3 4 5))
(tail-recursion (list 1 2 3 4 5) `())

Elixir

defmodule Double do
    def recurssion([head|tail]) do
        [2 * head | recurssion(tail)]
    end

    def recurssion([]) do
        []
    end

    def tail_recursion([head|tail], new_list) do
        new_list = new_list ++ [2 * head]
        tail_recursion(tail, new_list)
    end

    def tail_recursion([], new_list) do
        new_list
    end
end

Double.recurssion([1, 2, 3, 4, 5])
Double.tail_recursion([1, 2, 3, 4, 5], [])

了解递归和尾递归之后,另外一个需要了解就是并不是每个语言都支持尾递归。上述的 python就不支持。Python使用尾递归,在数据量稍大的时候会溢出。此外,像 Scheme和Elixir这些语言则很好的支持。当需要在遍历的时候写逻辑,可以抽象出逻辑为一个个函数,更有利于代码的模块化和复用。

总结一下,普通递归过程是函数调用,涉及返回地址、函数参数、寄存器值等压栈等,而尾递归压栈操作并无必要,不会有中间结果需要缓存。通常是语言层面是否支持,编译器或解释器中进行优化。

 



作者:人世间
链接:http://www.jianshu.com/p/1f69cb4525ec
來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 

分享到:
评论

相关推荐

    C#中的尾递归与Continuation详解

    递归与尾递归 关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度: 代码如下: public class Node {  public ...

    详解python使用递归、尾递归、循环三种方式实现斐波那契数列

    但Python标准解释器并未对尾递归进行优化,所以尾递归在Python中的效果并不明显。 3. **循环**: 循环是最有效的方法,如 `Fib_circle` 函数所示。通过循环,我们可以避免递归带来的额外开销,只需线性时间复杂度...

    关于尾递归的使用详解

    **举例:菲波那契数列的非尾递归与尾递归实现** 在PHP中,非尾递归实现的菲波那契数列如下: ```php function fibonacci($n) { if ($n ) { return $n; } return fibonacci($n – 1) + fibonacci($n – 2); } ```...

    尾递归详细总结分析

    尾递归与Continuation递归与尾递归关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度: 代码如下:public class ...

    Fibonacci数列的四种解法:递归、存储优化的递归、自下而上的递归(迭代法)、尾递归

    fibonacci数列的各种解法,递归、存储优化的递归、自下而上的递归(迭代法)、尾递归。其中分析内容请移步我的博客、

    递归算法与非递归转化

    直接转换法通常用来消除尾递归和单向递归,将递归结构用循环结构来替代。尾递归是指在递归算法中,递归调用语句只有一个,而且是处在算法的最后。当递归调用返回时,是返回到上一层的调用点,而不是返回到初始调用点...

    递归与迭代算法及其在JAVA语言中的应用.pdf

    因此,有时候通过“尾递归”优化或是将递归改写为迭代来提高效率是必要的。 递归与迭代在Java中不仅可以应用于基础算法的实现,还可以应用于更复杂的数据结构和算法,例如树的遍历、图的搜索等。在处理这类问题时,...

    尾递归.cpp

    /*int f(int n,int a1,int a2) { if(n) return a1; else return f(n - 1, a2, a1 + a2); }*/

    蓝桥杯大赛 青少年创意编程 Python组 资料集-2022.01.21.pdf

    * Python 算法之递归与尾递归:该资源提供了 Python 算法之递归与尾递归的解析和解答,涵盖了递归和尾递归算法的基本概念和应用。 * Python 实现欧拉函数:该资源提供了 Python 实现欧拉函数的解析和解答,涵盖了...

    icarnegie ssd5第三章答案

    8. **递归与尾递归**: - **尾递归**:如果递归调用是函数体的最后一条语句,且返回值直接是递归调用的结果,这样的递归被称为尾递归。尾递归可以被优化,避免额外的栈空间开销。 9. **递归的例子**: - **阶乘...

    Python递归及尾递归优化操作实例分析

    虽然这段代码在Python中并不会自动优化,但如果在支持尾递归优化的语言(如Scheme)中,它将避免栈溢出。 除了阶乘,递归还常用于解决各种问题,如汉诺塔问题。汉诺塔是一个经典的递归问题,目标是将所有盘子从柱子...

    C#函数式编程中的递归调用之尾递归详解

    下面我们直接切入正题,开始介绍尾递归。 尾递归 普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆栈上的图来展示具体的差距在哪,首先我们来看看普通的递归调用的...

    .net 递归算法 .net 递归算法.net 递归算法

    因此,非尾递归或深度较大的递归可能需要优化,如使用迭代代替或者尾递归优化。 - **终止条件**:确保每个递归调用都向基础情况靠近,否则可能会导致无限递归。 - **理解问题与分解**:正确理解和分解问题是成功...

    递归教程 全套讲解

    1. **尾递归**:如果递归调用是函数的最后一个操作,并且返回值直接传递给调用者,那么这个递归被称为尾递归。尾递归可以通过编译器或解释器优化,避免额外的栈空间开销。 2. **记忆化**:对于重复计算的问题,可以...

    递归与分治算法的设计

    递归与分治算法是计算机科学中非常重要的概念,它们在解决复杂问题时起到了关键作用。递归是一种自相似的解决问题的方法,它通过调用自身来处理问题的子集,直至达到基本情况,然后逐步合并结果以得出最终答案。分治...

    递归函数示意图.pdf

    在上述内容中,首先展示了一个简单递归函数的示例,然后又通过添加辅助函数进行了尾递归优化。 在简单递归函数的示例中,定义了一个名为f(x)的函数。当x为1时,函数返回自身值1;否则,函数返回当前值x乘以f(x-1)的...

    栈与递归的动画演示(swf)

    6. **优化**:动画可能涵盖了尾递归的概念,以及编译器如何优化尾递归调用以避免栈空间的浪费。 通过这样的动画演示,学习者能够直观地理解栈的工作原理和递归的执行过程,这对于理解和解决复杂问题至关重要。在...

Global site tag (gtag.js) - Google Analytics