设为首页 加入收藏

TOP

计算机中的黑魔法:尾递归(一)
2015-07-16 12:55:15 来源: 作者: 【 】 浏览:7
Tags:计算机 魔法

声明:本文是作者在学习SICP有关过程抽象知识的理解,由于书中的语句有些晦涩,所以将作者的理解共享给大家希望帮助一些朋友。原书对尾递归并没有太多介绍,但是这里给出了详细的解释。


目前是凌晨1点48分。嗯,刚刚写完这篇日志。忍不住想说点什么,或许是当一个不好的书托。可能这些内容对于很多人来说是没有用的,他们甚至会鄙视我写的东西,觉得为这些东西花费时间不值得。对于这些人,我想说,每一个对计算机有着浓厚兴趣的人,都有着一个想够彻头彻尾了解每天超过12小时面对的这台机器是如何工作的愿望。像SICP和CSAPP这种书无疑是枯燥的,你可能一天看下来,仔细品读的话只能看三四页,但是如何你能够真正理解这几页书的内涵的话,那么收获将是巨大的。从去年5月份,陈大师推荐我读CSAPP之后,我才真正找到了那种看上去很枯燥,但是一读起来就欲罢不能的书。这类的书读起来都很困难,如果你带着很强的功利性来读的话,是不可能读下去的。我想了想,能够支撑我一天坐在那琢磨这几页书中说的核心意思的力量,就是来源于我真正想了解计算机是怎么工作的,获取这种信息对于我来说是非常快乐的,这种感觉是奇妙的。


函数一般是指数学上面的一种计算形式,偏向说明语句,描述如何根据给定的一个或者几个参数去计算出一个值。如SICP中关于求一个数的平方根的函数,我们在该函数的说明中可以推断出关于平方根的一些具体性质和一般性的事实,但是我们无从得知计算一个数的平方根的具体方法。你可能会说,求平方根不就是开根号么。可是你有没有想过,[开根号]也仅仅是一个说明性的语句,具体怎么计算你还是不知道的。延伸到计算机当中的函数,其实和数学上面的函数意义是相同的,我们只不过是换成高级程序设计语言来写我们对于一个函数一般性事实的说明,实际上我们并没有给出一个具体的计算过程。比如求平方根,如果我们是调用math.h中的库函数来求的话:sqrt(x*1.0),这种形式只是一个说明型的语句,我们利用这个语句来指导计算机去计算,但实际上这个函数并没有提供具体的计算过程。计算机当中的解释器就负责把这种说明性语句转化成真正的计算过程(期待到时候写一个解释器哇)。


其实感觉这两者的区别就和写作文一样,一个是提纲,另外一个是具体的内容。


SICP中关于求一个数平方根的问题,使用的是牛顿的逐步逼近法则,不断的去求新的猜测值,直到结果满足一定的精度结束。求平方根是一个大的功能,想要完成这个大的功能还需要一些小的功能来辅助。


我们把整个一个大的计算过程分为几个部分,每一个部分都能单独完成其中的一个小功能,他们组合起来又能够完成最终的功能。所谓过程的抽象和C++中的面向对象思想是和相像的,没准都是一个东西,我不太确定,但是过程的抽象说的都是一个意思,就是对创造者来说重要的是过程的实现,而对于使用者来说,过程的抽象可以屏蔽掉内部实现的细节,从而减轻使用者的负担,只关心这个过程的『黑盒』能够做什么。所以这样一来就增加了程序局部的可替换性,因为对于实现一个功能来说,过程的内部实现可以多种多样。


大家都知道调用函数或者过程的时候,有的时候需要传递一些合适的参数,在调用的过程中,函数的形式参数的名字对我们来说其实并不重要,相对于使用者来说确实是这样。但是对于过程的设计者来说,形式参数的名字,或者说是局部变量的名字,对于整个过程能够正常的执行就非常重要了。这也是过程抽象当中的一个细节,计算机把一些复杂的东西封装到了内部。


为什么说约束变量的名字对于过程的执行是非常重要的呢。很直接的一个理解就是:局部变量可以在作用域内可以屏蔽同名的全局变量,类比一下,约束变量在相应的作用域内会屏蔽同名的自由变量。


举例来讲,在上面这个过程中,guess 和 x 是good-enough?这个过程的两个约束变量,<, abs, -, square都是自由变量。自由变量是和环境相关的,他们已经有了确定的意义来保持他们正常的执行,但是如果现在guess这个约束变量改名为abs,将会导致abs这个自由变量的意义被约束变量屏蔽,过程中出现的abs为约束变量,不在具有自由变量当中求绝对值的功能。


所以说,对于过程的设计者而言,过程实现中,自由变量和约束变量的名字都在哪些地方用到是一个非常需要注意的地方,一个过程的顺利执行依赖于约束变量和自由变量的名字不同,并且自由变量的意义正确。


这一点的抽象就更加为我们过程的使用者考虑,它也是一种局部的概念,像局部变量一样,只有内部的作用域可以访问并且识别他,作用域之外是不知道作用域内有这个东西的。只不过我们之前抽象的是变量,这次抽象的是过程。


sicp中的求平方根的过程,和用户交互的仅仅是一个接口sqrt,其中的子过程的具体实现细节都被抽象一个个的【黑箱】。但是对于用户来说,这些实现计算过程的黑箱是没有用的,只会扰乱他们的思维,并且在用户想自定义一个与这些黑箱中的某一个同名的过程的时候是不被允许的(不能在一个作用域内定义两个同名的过程),这些有着具体功能的【黑箱】污染了全局名字环境。


对于一个结构局部的东西,我们更好的方式是把他们定义在这个结构的内部,而不应该放在全局环境范围内,因为你需要的并不是其他人也需要的,如果其他人不需要这些东西,那么他们的命名就有可能造成过程调用的命名冲突,造成程序混乱。


scheme支持在过程的内部再定义新的过程,内部新定义的过程只能在内部作用域可见,外部是不知道有内部这些过程的定义的。


根据上面的例子来看,我们就把求平方根这计算过程中用到的小黑箱一个一个的都定义在了这个过程的内部 ,过程之外看不到他们,这种嵌套的过程定义就叫做块结构。局部化使程序更清晰,减少全局名字,减少相互干扰。除此之外,由于将一些黑箱进行了内部定义之后,还有一处可以改进的地方,就是参数x的使用。由于局部过程的形参都在x的作用域内,我们就不必在局部过程中再传递x了,直接调用即可。相对于局部过程来说,现在的X由原来的约束变量,变为现在的自由变量了。


我们可以用不同种类的过程来完成同一件任务,那么在真正的程序设计的时候,我们应该选用哪种方式呢?按照之前数据结构的角度来看,我们有两个指标:时间复杂度和空间复杂度。不同的过程自然有这不同的计算过程,我们应该通过衡量两个不同计算过程的上述的两个指标来确定最终选择哪一个作为最终版本。这就要求我们能够准确的观察到任何一个过程执行的过程和执行的效果,以及执行过程中所耗费的各种资源。


在以实例说明递归和迭代的区别之前,首先要明确这样两个概念的区别:


假如现在有一个求n的阶乘的需求:我们有正向和逆向两种思路来求得结果


算法如下:n! = n (n-1)! = n (n-1) (n-2)!…..2 1 = n * (n-1)!


有了具体的计算进程,我们可以根据它来写一个过程


PS:可以看出这个过程在定义的时候调用了自身


假设现在我们想计算(factorial 6),根据shceme的代换模型推导,可以得出如下计算进程图示:



从图示中我们可以看出,当我们计算(factorial 6)的时候,根据计算过程的定义,要执行(* 6 (factorial (- 6 1)))。通过这一个复合的表达式我们就可以看出来,(factorial 6)这一步的计算结果并没有计算出来,它需要计算下一个状态来辅助它得出(factorial 6)这一步的计算结果。因为本次的计算没有完成要进行其他的运算来辅助完成,那么本次的运算就算是中断了,既然中断了,我们就需要保存现场,保存此次计算过程目前的状态(变量等等一些资源)以便当下一状态的计算结果出来之后能够顺利的衔接上,从而计算出最终的结果。以后的计算进程仍然是按照这种形式来的,直到遇到边界。


到此为止,我们就知道,这个计

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Hibernate性能优化之EHCache缓存 下一篇C#中HttpClient使用注意:预热与..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: