• 递归“赏析”


    前言:

    以往只会验证已有的递归算法,但不能做核心的设计思路;

    建模问题!!!!!!!!!

    斐波那契数列

    Fn = Fn-1 + Fn-2;

    F0 = 0;

    F1 = 1;

    在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

    举个例子,我们来计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示,可以看出:

    fact(n) = n! = 1 x 2 x 3 x ... x (n-1) x n = (n-1)! x n = fact(n-1) x n

    所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。

    于是,fact(n)用递归的方式写出来就是:

    def fact(n):

        if n==1:

            return 1

    return n * fact(n - 1)

    1.        什么是递归

    迭代的是人,递归的是神

    –L. Peter Deutsch

    简单的定义: “当函数直接或者间接调用自己时,则发生了递归.” 说起来简单, 但是理解起来复杂, 因为递归并不直观, 也不符合我们的思维习惯, 相对于递归, 我们更加容易理解迭代. 因为我们日常生活中的思维方式就是一步接一步的, 并且能够理解一件事情做了N遍这个概念. 而我们日常生活中几乎不会有递归思维的出现.

    2.        理解递归(数学归纳法)

    在初学递归的时候, 看到一个递归实现, 我们总是难免陷入不停的回溯验证之中, 因为回溯就像反过来思考迭代, 这是我们习惯的思维方式, 但是实际上递归不需要这样来验证. 比如, 另外一个常见的例子是阶乘的计算. 阶乘的定义: “一个正整数的阶乘(英语:factorial)是所有小于或等于该数的正整数的积,并且0的阶乘为1。” 以下是Ruby的实现:

    def factorial(n)

      if n <= 1 then

        return 1

      else

        return n * factorial(n - 1)

      end

    end

    我们怎么判断这个阶乘的递归计算是否是正确的呢? 先别说测试, 我说我们读代码的时候怎么判断呢?

    回溯的思考方式是这么验证的, 比如当n = 4时, 那么factoria(4)等于4 * factoria(3), 而factoria(3)等于3 * factoria(2), factoria(2)等于2 * factoria(1), 等于2 * 1, 所以factoria(4)等于4 * 3 * 2 * 1. 这个结果正好等于阶乘4的迭代定义.

    用回溯的方式思考虽然可以验证当n = 某个较小数值是否正确, 但是其实无益于理解.

    Paul Graham提到一种方法, 给我很大启发, 该方法如下:

     

    n=0, 1的时候, 结果正确.

    假设函数对于n是正确的, 函数对n+1结果也正确.

    如果这两点是成立的,我们知道这个函数对于所有可能的n都是正确的。

     

    3.        使用递归

     

    既然递归比迭代要难以理解, 为啥我们还需要递归呢? 从上面的例子来看, 自然意义不大, 但是很多东西的确用递归思维会更加简单……

    经典的例子就是斐波那契数列, 在数学上, 斐波那契数列就是用递归来定义的:

    ·F0 = 0

    ·F1 = 1

    ·Fn = Fn – 1 + Fn – 2

    有了递归的算法, 用程序实现实在再简单不过了:

    def fibonacci(n)

      if n == 0 then

        return 0

      elsif n == 1 then

        return 1

      else

        return fibonacci(n - 1) + fibonacci(n - 2)

      end

    end

    改为用迭代实现呢? 你可以试试.

    上面讲了怎么理解递归是正确的, 同时可以看到在有递归算法描述后, 其实程序很容易写, 那么最关键的问题就是, 我们怎么找到一个问题的递归算法呢?

    Paul Graham提到, 你只需要做两件事情:

    你必须要示范如何解决问题的一般情况, 通过将问题切分成有限小并更小的子问题.

    你必须要示范如何通过有限的步骤, 来解决最小的问题(基本用例).

    如果这两件事完成了, 那问题就解决了. 因为递归每次都将问题变得更小, 而一个有限的问题终究会被解决的, 而最小的问题仅需几个有限的步骤就能解决.

    4.        尾递归介绍

      对于递归函数的使用,人们所关心的一个问题是栈空间的增长。确实,随着被调用次数的增加,某些种类的递归函数会线性地增加栈空间的使用 —— 不过,有一类函数,即尾部递归函数,不管递归有多深,栈的大小都保持不变。尾递归属于线性递归,更准确的说是线性递归的子集。

      函数所做的最后一件事情是一个函数调用(递归的或者非递归的),这被称为 尾部调用(tail-call)。使用尾部调用的递归称为 尾部递归。当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。

  • 相关阅读:
    TODO 模板实践
    C++类继承方式及实践
    【转】C++友元
    C++面向对象实践
    数组指针实践
    引用&指针交换函数实践
    左值引用&右值引用实践【TODO】
    const变量的修改实践
    【转】c语言动态与静态分配
    【转】数组指针&指针数组
  • 原文地址:https://www.cnblogs.com/cenmny/p/7906311.html
Copyright © 2020-2023  润新知