热门标签 | HotTags
当前位置:  开发笔记 > 人工智能 > 正文

递归计算过程与迭代计算过程

递归是实现程序计算过程中的描述过程的基本模式之一,在讨论递归的问题前我们必须十分小心,因为递归包含两个方面的内容,一个是递归的计算过程,一个是递归过程,后者是语法上的事实而前者是概念上的计算过程,事实上在程序上我们也许是使用循环来实现的。

最近重新看SICP,写点感想。下面是关于递归与迭代计算的一些知识,SICP 1.2.1。

递归

递归是实现程序计算过程中的描述过程的基本模式之一,在讨论递归的问题前我们必须十分小心,因为递归包含两个方面的内容,一个是递归的计算过程,一个是递归过程,后者是语法上的事实而前者是概念上的计算过程,事实上在程序上我们也许是使用循环来实现的。

递归计算过程和我们常说的递归过程不是一回事。

  • 递归过程:“当我们说一个过程是递归的时候,论述的是一个语法形式上的事实,说明这个过程的定义中(直接或者间接地)引用了该过程本身。”
  • 递归计算过程:“在说某一计算过程具有某种模式时(例如,线性递归),我们说的是这一计算过程的进展方式, 而不是相应过程书写上的语法形式。”

一般在讨论递归的时候都喜欢用斐波那契数列来作为例子,斐波那契的算法也很简单,算法如下:

def Fib(n):  

    if (n <1):  
        return 0  

    elif (n <= 2):  
        return 1  

    else:  
        return Fib(n-1)+Fib(n-2)

具体C语言的例子。

#include "stdio.h"
#include "math.h"

int factorial(int n);

int main(void)
{
    int i, n, rs;

    printf("请输入斐波那契数n:");
    scanf("%d",&n);

    for(i = 1; i <=n; i++)
    {
        rs = factorial(i);
        printf("%d ", rs);
    }

    return 0;
}

// 递归计算过程
int factorial(int n)
{
    if(n <= 2)
    {
        return 1;
    }
    else
    {
        return factorial(n-1) + factorial(n-2);
    }
}

程序运行:

请输入斐波那契数n:12
1 1 2 3 5 8 13 21 34 55 89 144

我们假设n=6,那么得到的计算过程就是,要计算Fib(6)就得计算Fib(5)和Fib(4),以此类推,如下图:

我们可以看到过程如同一棵倒置的树,这种方式被称之为树形递归,也被称之为线性递归。这种递归的方式非常的直白,很好理解其计算过程,一般很多人写递归都会下意识的采用这种方式。

但是缺点也是很明显的,从其计算过程可以看出,经过了很多冗余的计算,并且消耗了大量的调用堆栈,这个消耗是指数级增长的,经常有人说调用堆栈很容易在很短的递归过程就耗光了,多半就是采用了线性递归造成的。线性递归的过程可用下图描述,可以清晰的看到展开收拢的过程:

(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

迭代

与递归计算过程相对应的,是迭代计算过程。

除了这种递归方式还有另外一种实现递归的方式,同样是上面的斐波那契数作为例子,这次我们不按照斐波那契的定义入手,我们从正常产生数列的过程入手来实现,0,1,的情况很简单可以直接返回,之后的计算过程就是累加,我们在递归的过程中要保持状态,这个状态要保持三个数,也就是上两个数和迭代的步数,所以我们定义的方法为:

def Fib(n,b1=1,b2=1,c=3):

    if n <= 2:
        return 1

    else:
        if n==c:
            return b1+b2

        else:
            return Fib(n,b1=b2,b2=b1+b2,c=c+1)

这种方法我们在每一次递归的过程中保持了上一次计算的状态,所以称之为“线性迭代过程”,也就是俗称的尾递归。由于每一步计算都保持了状态所以消除了冗余计算,所以这种方式的效率明显高于前一种,其计算过程如下:

fib(6)
fib  0,0,1
fib  0,1,2
fib  1,2,3
fib  2,3,4
fib  3,5,5
fib  5,8,6

这两种递归方式之间是可以转换的,凡是可以通过固定数量状态来描述中间计算过程的递归过程都可以通过线性迭代来表示。

“迭代计算过程是用固定数目的状态变量描述的计算过程,并存在着一套固定的规则,描述了计算过程从一个状态到下一状态转换时,这些变量的更新方式,还有一个(可能有的)结束检测,它描述这一计算过程应该中止的条件。”

以计算n的阶乘为例,其递归写为:

// 递归计算过程
function factorial(n){
     if(n == 1) {
          return 1;
     }
     return n * f(n-1);
}

同样是计算n的阶乘,还可以这样设计:

// 迭代计算过程
function factorial(n){
     return factIterator(1, 1, n);
}

function factIterator(result, counter, maxCount){
     if(counter > maxCount){
          return result;
     }
     return factIterator((counter * result), counter + 1, maxCount);
}

它的执行过程为:

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

虽然factIterator方法调用了它自己,但从它的执行过程里,所需要的所有的东西就是result,counter,和maxCount。所以它是迭代计算过程。这个过程在继续调用自身时不需要增加存储,这样的过程叫尾递归。

尾递归还可以用循环来代替:

function fib(n){
     var a=0, b=1;
     for(var i=0;i<=n;i++){
          var temp = a+b;
          a = b;
          b = temp;
     }
     return b;
}

递归和迭代

递归计算过程更自然,更直截了当,可以帮助我们理解和设计程序。而要规划出一个迭代计算过程,则需设计出各个状态变量,找到迭代规律,并不是所有的递归计算过程都可以很容易的整理成迭代计算过程。

但递归计算过程会比迭代计算过程低效。

上面计算阶乘的递归计算过程属于线性递归,步骤数目的增长正比于输入n。也就是说,这个过程所需步骤的增长为O(n) ,空间需求的增长也为O(n) 。对于迭代的阶乘,步数还是O(n)而空间是O(1) ,也就是常数。

再来看斐波那契数列的递归与迭代的实现吧。

递归计算过程:

// 递归计算过程
function fib(n){
     if(n <= 1){
          return n;
     }
     return fib(n-1) + fib(n-2);
}

迭代计算过程、尾递归:

// 迭代计算过程、尾递归
function fib(n){
     return fibIterator(1, 0, n);
}

function fibIterator(a, b, counter){
     if(counter== 0){
          return b;
     }
     return fibIterator((a+b), a, counter-1)
}

斐波那契数列的递归计算过程属于树形递归,画一下它的展开方式就可以看到。它的步数是以指数方式增长的,这是一种非常夸张的增长方式,规模每增加1,都将导致所用的资源按照某个常数倍增长。而迭代计算过程的步骤增长依然是O(n),线性增长,也就是规模增长一倍,所用的资源也增加一倍。

有时候说要减少递归,就是要减少递归计算过程,用更高效的方法代替。

我们也发现,其实尾递归的过程和循环基本上是等价的,我们可以将尾递归的过程很方便到用循环来代替,所以很多的语言对尾递归提供了编译级别的优化,也就是将尾递归在编译期转化成循环的代码。不过对于没有提供尾递归优化的语言来说也是很有意义的,比如python的默认调用堆栈长度是1000,如果用线性递归很快就会消耗光,但是尾递归就不会,比如尾递归的Fib函数,用Fib(1001)调用没问题的而且跑得飞快,Fib(1002)的时候才堆栈溢出。但是如果是线性递归的方式计算n=30的时候就能明显感觉到速度变慢,40以上基本就挂了。

这里我无意对比两种方式的优劣,也许线性递归性能有差距但是它的可读性非常的强,几乎就等同于公式的直接描述,所以可以根据计算规模来合理选用。

延伸阅读

此文章所在专题列表如下:

  1. 漫谈递归:递归的思想
  2. 漫谈递归:递归需要满足的两个条件
  3. 漫谈递归:字符串回文现象的递归判断
  4. 漫谈递归:二分查找算法的递归实现
  5. 漫谈递归:递归的效率问题
  6. 漫谈递归:递归与循环
  7. 漫谈递归:循环与迭代是一回事吗?
  8. 递归计算过程与迭代计算过程
  9. 漫谈递归:从斐波那契开始了解尾递归
  10. 漫谈递归:尾递归与CPS
  11. 漫谈递归:补充一些Continuation的知识
  12. 漫谈递归:PHP里的尾递归及其优化
  13. 漫谈递归:从汇编看尾递归的优化

本文地址:http://www.nowamagic.net/librarys/veda/detail/2280,欢迎访问原出处。


推荐阅读
  • 本文详细解析了JavaScript中相称性推断的知识点,包括严厉相称和宽松相称的区别,以及范例转换的规则。针对不同类型的范例值,如差别范例值、统一类的原始范例值和统一类的复合范例值,都给出了具体的比较方法。对于宽松相称的情况,也解释了原始范例值和对象之间的比较规则。通过本文的学习,读者可以更好地理解JavaScript中相称性推断的概念和应用。 ... [详细]
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • qt学习(六)数据库注册用户的实现方法
    本文介绍了在qt学习中实现数据库注册用户的方法,包括登录按钮按下后出现注册页面、账号可用性判断、密码格式判断、邮箱格式判断等步骤。具体实现过程包括UI设计、数据库的创建和各个模块调用数据内容。 ... [详细]
  • “你永远都不知道明天和‘公司的意外’哪个先来。”疫情期间,这是我们最战战兢兢的心情。但是显然,有些人体会不了。这份行业数据,让笔者“柠檬” ... [详细]
  • 生成对抗式网络GAN及其衍生CGAN、DCGAN、WGAN、LSGAN、BEGAN介绍
    一、GAN原理介绍学习GAN的第一篇论文当然由是IanGoodfellow于2014年发表的GenerativeAdversarialNetworks(论文下载链接arxiv:[h ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 无线认证设置故障排除方法及注意事项
    本文介绍了解决无线认证设置故障的方法和注意事项,包括检查无线路由器工作状态、关闭手机休眠状态下的网络设置、重启路由器、更改认证类型、恢复出厂设置和手机网络设置等。通过这些方法,可以解决无线认证设置可能出现的问题,确保无线网络正常连接和上网。同时,还提供了一些注意事项,以便用户在进行无线认证设置时能够正确操作。 ... [详细]
  • 本文介绍了游戏开发中的人工智能技术,包括定性行为和非定性行为的分类。定性行为是指特定且可预测的行为,而非定性行为则具有一定程度的不确定性。其中,追逐算法是定性行为的具体实例。 ... [详细]
  • JavaScript设计模式之策略模式(Strategy Pattern)的优势及应用
    本文介绍了JavaScript设计模式之策略模式(Strategy Pattern)的定义和优势,策略模式可以避免代码中的多重判断条件,体现了开放-封闭原则。同时,策略模式的应用可以使系统的算法重复利用,避免复制粘贴。然而,策略模式也会增加策略类的数量,违反最少知识原则,需要了解各种策略类才能更好地应用于业务中。本文还以员工年终奖的计算为例,说明了策略模式的应用场景和实现方式。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • 本文详细介绍了相机防抖的设置方法和使用技巧,包括索尼防抖设置、VR和Stabilizer档位的选择、机身菜单设置等。同时解释了相机防抖的原理,包括电子防抖和光学防抖的区别,以及它们对画质细节的影响。此外,还提到了一些运动相机的防抖方法,如大疆的Osmo Action的Rock Steady技术。通过本文,你将更好地理解相机防抖的重要性和使用技巧,提高拍摄体验。 ... [详细]
author-avatar
手机用户2602891751
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有