MIT 6.001 Structure And Interpretation Of Computer Programs(SICP)学习笔记(二)

Lecture 3 Procedures and Process

        在本讲座中,我们将把上一讲中介绍的Scheme 的基本部分放在一起,以便开始捕获程序内的计算过程。为此,我们将引入一种计算模型,称为替代模型(substitution model)。我们将展示该模型如何帮助我们将设计过程中所做的选择与使用该过程时将发生的实际评估过程联系起来。

        因此,我们将首先查看替换模型的示例,以确保您了解其机制,然后我们将使用它来检查创建过程的两种不同方法。

substitution model

        替换模型的作用是为我们提供一种确定表达式如何演变的方法。在检查替代模型时,我们强调这并不是计算机内部发生的情况的完美模型。事实上,在本学期晚些时候,我们将看到一个更详细、更准确的计算模型,但替代模型足以满足我们的目的。

        事实上,我们并不真正关心使用替换模型来计算表达式的实际值。我们可以使用计算机来做到这一点。相反,我们希望使用替代模型来理解评估过程,以便我们可以使用这种理解来推理设计过程中的选择。有了这种理解,我们就可以逆向工作,使用所需的评估模式来帮助我们设计正确的过程来生成该评估模式。

替代模型的规则:

        如果各位认真听了课,那么毫无疑问,这张规则图能够轻易看懂,因此不再赘述~

简单的例子

         首先,这是一个复合表达式,然后对于square这一个name,返回与其配对的平方procedure,第二个子表达式就是self-evaluating也就是4,然后lambda规则要求使用4代替所有出现x的地方,并用该实例化的主体表达式替换原始表达式。那么现在就简化成了(* 4 4).这也是一个复合表达式,因此我们再次获得子表达式的值。在本例中,使用的procedure(*)是primitive procedure,因此我们只需应用它来将表达式减少到其最终值 16。

一个更加细节的例子。

         首先创建了两个procedure,他们被建立并且与对应的name配对。

        按照顺序,先对子表达式,square返回平方的过程,然后使用3代替形式参数,然后简化为(* 3 3),再到(average 5 9),average返回求和平均,同样用5、9代替,然后得到全部是primitive procedure的表达式,求值即可。

        上图中这种就是application order evaluation model。特别注意,在这个模型下,我需要首先获取操作数的值,这导致我在应用average之前先评估内部复合表达式。一旦我有了简单的值,我就可以继续替换。在本例中,我使用与average相关的过程主体,现在用 5 和 9 代替该主体中的 x 和 y。然后继续。我将规则递归地应用于每个子表达式。对于复合表达式,我必须将其减少为更简单的值,然后才能继续表达式的其余部分。

 不那么简单的例子

        假设我想计算 n 的阶乘,它被定义为 n 与 n-1 的乘积,依此类推直至 1。请注意,我假设 n 是一个整数并且大于或等于 1。

        那么如何使用loop的思想简化这个计算过程呢?

        核心的一点就是阶乘是指n*(n-1)*....*1的结果。我们能够发现,除了第一个乘数以外的部分是n-1的阶乘。那么现在我们就将一个复杂的问题简化成了同样问题的简化版本,再加上一些简单的操作。

        那么,接下来就看看代码如何构建吧。

\\
(define fact (lambda (n) (if (= n 1) 1 (* n (fact (- n 1))))))

        可以看到,我们再次使用了if这一特殊形式,第一个字表达式叫谓词predicate,第二个是consequent,第三个是alternative。并且其中还使用了“=”这一符号(判断是否相等,并且返回布尔值)

        if表达式首先计算predicate的值,并且在这个表达式上递归使用同一组规则,注意,这一操作是在它查看其他两个子表达式之前就进行的操作。另一个值得注意的点是,在 if 期间仅计算后续表达式和替代表达式其中的一个,具体哪一个由predicate返回的值决定(更加深入的描述是,根据返回的predicate的值,if表达式整个被consequent或者alternative取代,然后进行下一步计算)

        那么就我们的程序而言,我们判断n是否为1,如果是,那么fact返回1,如果不是那么就返回n*(n-1!)。

        下面仔细分析下(fact 3)的evaluate过程

        

         请注意红色所示的形式,其中的表达式被简化为更简单操作的模式(即primitive p'ro)和同一问题的更简单版本。这种展开一直持续到我们获得一个最内层子表达式仅涉及简单表达式和操作的嵌套表达式。此时将对延迟操作进行求值。这意味着事实上产生了我们所说的递归过程。在替换模型中,我们可以看到它的特点是一组延迟运算,其中乘法被延迟,而我们去获得事实的更简单版本的子计算。一旦我们得到了一个简单的案例,我们就可以开始累积那些堆积起来的操作(我们下次会再讨论这个想法。)

构建递归结构的思想步骤

        那么现在让我们退一步想想构建这样的递归过程的思想。一般来说,我们把这样的过程分为三个阶段。

1.wishful thinking

        具体来说就是,我先假设我有一个可以解决小于这一问题的方法,但是它只适用于较小的版本(在例子中就是假设我能够计算n-1的阶乘)

2.decompose the problem

        那么接下来就是如何通过简单的操作以及较小问题的解决方案的结合来解决较大问题,这样一来我们就实现了对于大问题的简化,变成了小问题以及一些简单的操作的组合。请注意,第二步往往需要一些创新性的思维,因此这一个步骤是我们在构建解决方案的时候就应该提前考虑到的。

3.identify non-decomposable(smallest)problem

        如果没有第三步,那么毫无疑问,第二步的简化将永无止境,所以第三步的关键就是,找到不可再简化(最简)的问题的解决方案(在本例子中就是,1!=1)

        如下图所示,这就是一个经典的递归格式,具有测试、基本情况和递归情况的递归算法的常见模式。if控制求值的顺序来决定我们是处于基础情况(base case)还是递归情况(recursive case),递归情况就控制问题的简化过程,直到我们的基本情况。

        

an iterative procedure for computing factorial(一种不延迟乘法的计算阶乘的迭代过程

        我们已经介绍了递归过程,现在我们已经看到了构建递归过程来实现阶乘的示例。现在,让我们看一下另一种过程,即计算阶乘的迭代过程。在此过程中,我们将了解如何开发具有不同演化的程序来计算相同的基本计算。

        回顾我们上一个设计,其关键是延迟计算,在获得最简单的子表达式之前,我们对于外层的*法都不进行计算,直到抵达base case(即1)时,才进行计算。那么这里出现一个问题,随着过程参数变大,我们所延迟的计算也不断增加,很明显这是一个增加计算机负担的行为。

        很自然的,我们就想看看是否有另一种计算阶乘的方法,而不需要所有这些延迟操作——计算不需要跟踪任何延迟的操作,而是可以执行其工作,而无需使用任何额外的内存来跟踪要做的事情。

        这就是我们的目标,以一种不以延迟乘法为特征的不同方式计算阶乘。

        首先想象一下,如果使用手算的方法,那么第一步就会是计算3*4=12,然后下一步就是使用12*2=24,然后计算24*1=24。这与递归的方法的区别在于,每一步中我只需要跟踪上一步的结果和下一步的乘数即可,而递归需要跟踪所有被延迟的乘法计算。毫无疑问,这种新的方法需要的空间保持在一个稳定的值,并且更小。

        如下图所示,其中一列代表我需要的每条信息,一行代表计算的每个步骤。这里的每一步都意味着如何从当前状态(由临时乘积和计数器捕获)转到这些参数的下一个状态,计数器告诉我下一步要相乘。现在需要制定一条规则来更改表中的值。特别是,为了获得product的下一个值,我将产品的当前值和计数器的当前值相乘,将它们相乘,这将成为产品列中的下一个条目。同样,需要一个规则来获取counter的下一个值——只需将当前行的计数器值加 1 即可,从而生成下一行的计数器值。

        这样一来,我就能够不断更新,直到counter列的数字是n+1的时候停止。然后最终的计算结果就是第n+1行与product列的交汇点的元素。

        那么现在,唯一确认做的另一件事是弄清楚如何开始这个过程。我的第一行将处理相当于基本情况的情况。 0!只是 1,所以我可以从 1 开始我的乘积,从 1 开始我的计数器,将 N 设置为我想要计算的任何值,然后我可以使用我刚才描述的规则开始这个表过程。

         下面是代码部分

\\主程序
(define ifact (lambda (n) (ifact_helper 1 1 n)))
\\执行过程
(define ifact_helper (lambda (product counter n) (if (> counter n) product (ifact_helper(* product counter) (+ counter 1) n))))

        定义了一个主程序,而主程序中却调用了一个子程序,而且是三个参数的。这是由于两个参数是用于存储product和counter的,而另外一个是用于验证停止的条件。正好满足if特殊形式的使用要求。

        子程序的流程和递归不同,具体如下,首先,有一个测试用例(predicate),告诉我们何时完成。这里我们没有base case,而是有一个返回值,即测试案例成立时给出的答案。迭代情况,即测试用例不正确时要做的事情,与我们的递归情况略有不同。此处,对过程的调用具有新参数,每个参数一个。请注意每种情况下的新参数如何从表中捕获更新规则。

        解决了对于原理图中的计算,那么就需要解决最后一个问题了,何时停止计算?同样的,可以使用一个if来解决,同时,由于我们知道answer就在product中,所以只需要返回它就可以了。

        注意上图中一件重要的事情。当将涉及 ifact-helper 的一个表达式的求值简化为涉及 ifact-helper 的另一个表达式,并具有一组不同的参数时,这里没有延迟操作,也就是说,在我们开始计算某些子问题时,没有必须跟踪的操作(简单来说就是,两个相邻的红色表达式之间,计算得到进一步进行但是二者需要的空间是一样大的,只是参数值改变了而已)。并且,计算这个表达式的答案就是直接计算第一个表达式的答案。我们可以用相同的行为重复这个过程,直到predicate为真。当获取子表达式的值时,没有延迟操作,也没有表达式存储。相反,捕获计算状态的变量会随着每次评估而变化。这与递归版本的阶乘的形状有很大不同。在那里,我们有一组延迟操作,并且表达式的大小随着每一步而增长。那么也就说明了这个方法和递归法的区别,本方法中没有延迟操作,并且只需要恒定的空间来跟踪计算。

        而递归版本有一个悬而未决的操作,它导致了过程演变的形状不同。它随着每次新的评估而增长,因为必须跟踪额外的待处理操作,包括与每个操作相关的信息。

        下面是关于此方法的总结(在图的下方有中文翻译供参考):

1.迭代算法空间大小稳定

2.如何设计一个迭代算法?

(1)找出一个累积阶乘答案的方法

(2)写出一个用于精确分析的表格,应当包括:

        初始化第一行

        其他行的更新规则

        如何知道何时停止

(3)将规则写成scheme代码

3. 当进程调用自己时,迭代算法不会延迟操作(计算)

proof methods of the correctness of our algorithms(验证代码正确的方法)

         通常来说,有以下四种方法,如上图所示。

1、权威者的证明。我们不敢不同意的人说这是对的

2、统计数据的证明。我们尝试了足够多的例子,然后认为代码是正确的

3、自信心的证明。我们真的真的真的相信我们的代码就是对的

4、形式证明。使用数学的逻辑来确定代码是正确的

        第四种是我们推荐的,并且我们将简单的看看第四种方法是如何实施的,并且尝试令学习者使用此证明方法背后的推理模式来帮助他们设计良好的代码。

        首先讲讲什么是形式证明。       

        从技术上讲,数学或逻辑命题的证明是一系列逻辑推论,从一组基本公理开始,引出我们试图证明的命题。现在,我们需要填写这些部分的详细信息。

        首先,命题基本上是一个要么真要么假的陈述。通常,在命题逻辑中,我们有一组原子命题,即简单的事实陈述。例如,“n=0”是一个原子命题。给定变量 n 的某个值,该陈述要么为真,要么为假(当然,假设 n 是一个数字)。

        更有趣的命题(以及我们更有可能有兴趣证明的事情)是通过组合更简单的命题而创建的。创建复合命题有五种标准方法:可以采用连词(其作用类似于“与”,意味着当且仅当两个单独命题都为真时复合命题为真);可以采用析取(其作用类似于“或”,意味着当且仅当单个命题中的一个或另一个为真时,复合命题才为真);可以对一个命题进行否定(这意味着当且仅当初始命题为假时该命题为真);我们可以创建蕴涵(这意味着如果 P 为真,则 Q 为真);我们可以建立等价关系(这意味着当且仅当 Q 为真时 P 为真)。

        请注意,复合命题可以包含本身就是复合命题的元素,以便我们可以创建深度嵌套的命题。

PS:简单来说,就是数学归纳法。

        那么就来在代码上试试吧!

        对于n=1,fact=1(此处,我们声明阶乘适用于大于等于1的整数)

        那么对于n,有ifact=n!。那么当n=n+1,fact=(* (+ n 1)(fact n))=(n+1)!,所以数学归纳法成立,则我们的代码适用于大于等于1的任意整数,即代码正确。

        现在,让我们把这个放在一起。从这个练习中得到的信息是,归纳法为理解、分析和证明递归过程定义的正确性提供了基础。

        而且,这种思维恰恰可以在我们设计程序时使用,而不仅仅是在分析程序时使用。当需要解决一个新问题时,确定基本情况并找到解决方案是很有价值的。然后,我们转向将问题分解为同一问题的更简单版本的问题,假设代码将解决该版本,并使用它来构建归纳步骤:如何解决问题的完整版本。

        所以我们强烈希望您在构建自己的程序时使用这种方法。

Lecture 4 Orders of Growth and Kinds of Procedures

        这节课主要是研究我们可以构建的procedures的类型。

        首先,回顾下substitution model,看看评估规则如何帮助我们确定由程序控制的过程的演变。然后我们将看到不同类型的过程演化如何发生,以及对计算资源的不同需求。我们将看到如何根据过程的增长顺序来正式描述这些差异。最后,我们将探讨不同增长类别的过程示例,帮助您开始了解过程设计中的选择如何影响过程实际使用的性能。

测量进程占用的资源(时间、空间)

        下面就是具体的量化代码性能的步骤了。

        在测量我们需要的空间量时,我们希望找到一个函数来测量延迟操作的数量,作为问题大小的函数。而对于测量时间量,我们希望找到一个函数来测量重写规则所经历的基本或原始步骤的数量,同样作为问题大小的函数。

        需要重写我们的表达式,其具体规则是:对于数字,built-in procedures,lambda表达式保持不变。而如果是定义的name abstraction则用其对应的值代替它(if的话,则评估其predicate,按照结果用consequent或者alternative)。如果是combination,则首先评估运算符表达式来获取过程,然后评估操作数来获取参数集。如果我们有一个原始的程序,我们就会做正确的事。否则,如果操作数是个复合表达式的name,则用复合表达式的主体替换整个表达式,并用参数替换其关联的参数。

        有了这个模型,我们就可以更正式地捕捉流程的演变。随着这些重写规则的发展,我们可以讨论进程的增长顺序。这衡量了随着这些规则的发展,一个进程将占用多少特定资源,通常我们将空间或时间衡量为资源。

        下面是对两个不同方法的代码的构建例子(n=4)。

        对于这种递归的方法,透过对黑色展开式的观察与分析,我们可以清楚的发现,对于占用的时间(这里以操作的步骤数作为量化单位),基本上需要参数(n)大小的两倍。而对于占用的空间,是随着n的增大而线性增大的。

         同样的,观察迭代方法的式子,我们发现,对于时间,和n的数量是相等的(即线性增长);而对于占用的空间,是一个恒定的值,不随n变化而改变。

        总结如下图。我们知道,FACT的时间占用是2n,而IFACT的时间占用是n,但是对于orders of growth来说并不重要,因为他们都是线性顺序增长。那么结论就是:这两个过程具有不同的进化形式。

        那么我们为什么要这样做呢?主要目标是让您开始认识不同类型的行为以及引起这些行为的相关过程形式。这将开始允许您在设计过程时逆向工作,通过使您能够可视化的看见以不同方式执行计算的结果。

斐波那契数列的实现以及占用资源分析

        在了解了两种不同类型的过程(一种是线性过程,一种是常数过程)之后,我们希望通过研究其他类型的过程来充实我们的过程库。下一个是指数过程的示例,涉及称为斐波那契的经典函数。它的定义是,如果其参数为 0,则其值为 0;如果其参数为 1,则其值为 1;对于所有其他正整数参数,其值是前面两个参数的值之和(具体如下图)

        我们将编写一个程序来计算斐波那契数列,特别是看看它如何产生不同类型的行为

        正好可以回顾下上个lecture的思想(笑)。(插句题外话,高中的时候觉得数学归纳法好难理解,好麻烦,遇到就不想做,现在觉得也不过如此。hhhhhhh)

        首先是,wishful thinking,对于n以及n以下的数,我们已经能够求出斐波那契数列的对应值了。那么问题就变成了,我们只需要计算出n的以及n-1的对应问题的值,再加起来就是n+1的问题的解了。需要注意的是,我们这里使用了两次wishful thinking而非lecture 3的一次。

        那么,设想一下,如果使用if,能够实现具体的功能吗?毫无疑问,可以,但是需要嵌套的使用if,这样一来,占用的空间,时间资源都会增加,所以我们引入新的special form——cond,将其用于本例子,见下图)

        Cond 的评估规则是非常好理解的(根据condition的不同,采取不同的操作,类似,switch)。 cond 由一组子句组成,每个子句中都有一个predicate clauses和一个或多个后续表达式。 Cond 首先评估第一个子句的谓词,在本例中为 (= n 0)。如果为 true,则我们依次计算该子句中的每个其他表达式,返回最后一个表达式的值作为整个 cond 的值。在该 cond 的第一个子句的情况下,即表达式 0。如果第一个子句的谓词为 false,我们将移至 cond 的下一个子句并重复该过程。无论 cond 中包含多少子句,都会继续这种情况,直到达到 true 谓词,或者到达带有特殊关键字 else 的子句。在后一种情况下,该谓词被视为 true,并且对后续表达式进行求值。(请注意,这里我们有两种基本情况,而不是一种。)

        另请注意,虽然这是一个递归过程,但它与我们之前的过程不同。这里,主体中的过程有两次递归调用,而不是只有一次。我们的问题是这是否会导致不同类型的行为,或者不同的增长顺序。

        经过分析递归过程,发现这确实产生一些不同,见下图。

         可以看出,如果要fib4,那么需要fib3以及fib2,而它们各自又需要另外两个fib。这导致了不同的orders of growth。

        使用t of n作为衡量时间(阶数)的函数。对于fib4,有 fib n= fib (n-1)+fib(n-2)而我们暂时将fib(n-1)与fib(n-2)视为相等,则为2fib(n-2),以此类推,最后会得出2的n/2次方这一结论。这是一个指数级增长的例子,这与我们之前看到的有很大不同。为了让自己相信这一点,假设每个步骤需要一秒钟,并看看当 n 变大时,指数过程与线性过程相比需要多少时间。

        而在空间方面,我们的树向我们展示了我们基本上有一个步骤的延迟操作,或者换句话说,该树的最大深度与问题的大小成线性关系,并且最大深度恰好捕获了延迟操作的最大数量。操作。让我们再快速看一下如何创建具有不同增长阶数的过程来计算相同的函数。

例子:a的b次方

        如何采用前面的思想构建实现a的b次方的计算呢?

        wishful思想告诉我们要解决从大规模问题简化成较小规模问题。那么对于a^b,其实只是b个a相乘,根据乘法的性质,我们可以将其改写成a*a^(b-1),这样我们就实现了问题的递归简化,然后对于base case,很明显,当b减小到0的时候就是base case亦即停止递归。对于具体代码的编写,相信也是非常容易能够实现的。

(define my-expt (lambda (a b) (if (= b 0) 1 (* a (my-expt a (- b 1))))))

        下面就请使用替代模型来分析下这个代码的orders of growth。我们可以推断出该过程在空间和时间上都具有线性增长(如下图)。时间很容易看出:参数大小的每一个增量都有一个额外的步骤。对于空间,我们看到过程的每个递归调用都有一个延迟操作,因此我们将线性数量地堆叠此类延迟操作,直到我们到达基本情况,此时我们可以开始完成延迟操作乘法并收集答案。

         正如我们之前所说的,我们期望有其他方法来创建过程来解决相同的问题,这样才便于进行对比来选出最好的过程。对于阶乘,我们使用了状态变量表的思想,以及用于更改这些变量值的更新规则。那么同样的,我们应该能够在这里做同样的事情。

        这个方法的思考方式很简单,而且都有迹可循:为完成计算步骤所需的每条信息设置一列,并为每个此类步骤使用一行。这里的想法非常简单。我们知道a的b次方就是b个连续的a相乘。所以我们可以只进行乘法,跟踪到目前为止累积的乘积,以及还剩下多少次乘法。因此,在一步之后,我们将需要做 a 和 b-1 的乘积。(如下图)

        

         

\\主程序
(define exp-i (lambda (a b) (exp-i-help 1 b a)))
\\辅助程序
(define exp-i-help (lambda (prod count a) (if (= count 0) prod (exp-i-help (* prod a) (- count 1) a))))

        现在,我们可以看到这个计算分为以下几个阶段。为了获得乘积的下一个值,我们将乘积的当前值与 a 的值相乘,然后保留。这会更新状态变量之一。为了获得计数器的下一个值,我们只需减 1,因为我们又进行了一次乘法。这会更新另一个状态变量。我们知道,当计数器减至零时,就不再需要进行乘法运算,因此我们就完成了。当我们到达那里时,答案就在prod中。最后,我们看到base case就是任何零次方都是 1。所以现在我们可以在过程中捕获这一点。与阶乘一样,我们将使用辅助过程。在这种情况下,我们可以看到该过程检查基本情况,如果存在,则仅返回 prod 的值。否则,它将计算简化为相同计算的更简单版本,并使用 prod 的新值和 count 的新值,这两个值都是通过使用我们刚刚看到的更新规则获得的。

        这里的增长顺序是什么?随着时间的推移,这仍然是线性的,因为 b 大小的每个增量都需要执行一次子计算。然而,在占用空间中,我们看到这里没有延迟的操作,因此占用空间是恒定的。因此,就像阶乘一样,我们可以创建不同的过程来计算幂,具有不同的行为类别。

         那么截至目前,我们已经看到了三种截然不同的程序:引起恒定空间的程序(迭代乘法实现n次方)、引起空间线性增长(递归实现n次方)的程序以及引起空间指数增长的程序(斐波那契数列)。

        那么接下来是第四种实现方法。

        首先需要考虑这样的情况。即如果 b 是偶数,那么我可以认识到,将 a 求 b 次方与先平方 a,然后将该结果求 b/2 次方相同。注意我做了什么。 a 的平方只是一次乘法,b 减 2 是一个简单的运算。但通过这样做,我已经将问题的规模缩小了一半。我只剩下 b/2 件事要考虑了。当然,请注意,我在这里依赖 b 作为偶数,因为在这种情况下 b/2 也是一个整数,并且我可以使用相同的机制来解决这个较小的问题。

        那么很明显的,上面的方法极大的缩小了计算的复杂度,但是也有一个很明显的缺陷,就是假如b是奇数的情况,那么在b为奇数时,下一步就采取b-1的方法简化,然后重新进行迭代。这样一来,无论什么情况,计算的复杂度在两步之后都会得到减半。代码如下

(define fast-exp-1 
   (lambda (a b) 
     (cond ((= b 1) a))
           ((even ? b) (fast-exp-1 (* a a) (/ b 2)))
           (else (* a (fast-exp-1 a (- b 1)))))))

        这个过程的形式有点不同。在偶数情况下,它看起来像一个迭代调用(仅更改过程的参数),在奇数情况下;它看起来像一个递归调用(具有延迟操作和参数减少)。因此,我预计在该过程的应用中会有一些延迟操作,但可能不会像前面的示例中那么多。

        但就时间而言会发生什么?由于过程中将问题减少了一半,因此可以希望这会带来比更好的性能。

        那么让我们来衡量一下这个过程的orders of growth。这里,n衡量问题的大小——b的大小。我们知道,如果b为偶数,则在第一步中问题大小会减少 2。如果b为奇数,则在一步中将其减少 1,使其成为偶数,以便在第二步中将其减少 2 .因此最多2步,问题大小就减半了。再经过 2 个步骤,它又被减半,因此经过 2k 个步骤,问题减少了 2^k 倍,或者说减半了 k 倍。

        如何找到需要执行此操作的次数?当问题大小仅为 1 时,即 k = log n 时,我们就完成了。所以这个过程有不同的行为,它是对数的。这实际上是一个非常有效的过程,为了让自己相信这一点,尝试同样的技巧,看看解决不同大小的问题需要多长时间,每秒一步,将其与线性和指数过程进行比较。可以发现,同样的,空间需求也随着大小的对数增长

        总而言之,我们现在已经看到替代模型如何帮助解释应用程序时发生的过程的演变。使用这个模型,我们已经看到,即使对于计算相同抽象函数的过程,也可能会发生非常不同的行为(即计算的过程非常不同),并且我们看到了如何根据空间和时间的orders of growth来表征这些差异。您已经可以看到有不同类别的算法:常数、线性、指数和对数。我们的部分任务就是学习如何识别与这些不同行为相关的程序类型,并学习如何使用这些知识来为特定问题设计有效的程序。

帕斯卡三角形实现以及不同procedures的orders of growth分析

         上图显示了帕斯卡三角形的元素。第一行只有一个元素,第二行有两个元素,第三行有三个元素,依此类推。显然,这些元素有一个我们需要理解的结构。对行进行排序,并枚举它们,标记第一行 n=0,第二行 n=1,依此类推(我们将看到这种标记选择可以使问题得到更清晰的描述)。使用这个标签,我们还可以看到第 n 行有 n+1 个元素。让我们使用符号 P(j,n) 来表示第 n 行的第 j 个元素。我们的目标是确定如何计算每行的所有元素。

        我们可以发现,对于(除了首尾的两个元素)每个元素,都能够由上一行对应位置元素和前一个位置元素的和得到。而每一行的首尾两个元素也是base case。

        那么我们就有两种基本情况,一种用于生成行的第一个元素,另一种用于生成行的最后一个元素。据此可以写出如下代码:

\\请注意,j,n分别表示第n行的第j个元素
(define pascal
   (lambda (j n)
       (cond ((= j 0) 1)
             ((= j n) 1)
             (else (+ (pascal ( j (- n 1))) (pascal ((- j 1) (- n 1))))
)))
\\这里我也不知道括号能不能放下写,我放下来纯粹是为了防止出错,我直接写的时候容易漏括号hhhhh。

        事实上,类似的分析将表明,这是一个时间上呈指数、空间上呈线性的过程。我们已经表明指数算法的成本很高。对于斐波那契数列,我们没有寻找任何其他方式来构建问题,但让我们尝试为帕斯卡数列做得更好。

        为了做得更好,我们必须回到最初的问题。来自组合学的一些信息告诉我们,实际上,帕斯卡三角形正在捕获从一组 n 个对象中选择一组 j 个对象的不同方法的数量(顺便说一句,这并不明显,所以只需接受它作为事实)。因此,行的第一个元素是不拾取任何对象的方式数,根据定义为 1。行的最后一个元素是从 n 个对象的集合中拾取 n 个对象的集合的方式数。由于我们选择对象的顺序并不重要,因此一组大小为 n 的集合中只有一个大小为 n 的子集,因此该元素为 1。一般情况是可以从大小为 n 的集合中创建大小为 j 的不同子集的数量。(如下图)

        

        那么我们就可以使用基于阶乘代码的方式来实现对pascal三角形的计算。

\\使用递归阶乘
(define pascal-1
    (lambda (j n)
       (/ (fact n) (* (fact (- n j)) (fact j)))
)
)
\\使用迭代阶乘
(define pascal-2
    (lambda (j n)
       (/ (ifact n) (* (ifact (- n j)) (ifact j)))
)
)

        这里我们使用阶乘三次,一次用于分子,两次用于分母。请注意,我们的procedure abstraction如何很好地将fact的计算过程细节与其在本例中的使用隔离开来,并且这里的代码清楚地表达了基于阶乘计算 Pascal 的思想。那么我们可以用这个版本的 Pascal 做得更好吗?当然!对于使用迭代阶乘实现的pascal-2,我们知道这个版本的ifact是线性的。我们的 Pascal 实现使用了三次不同的ifact,但这在时间上仍然是线性的,并且类似的分析让我们得出结论,这在空间上也是线性的。虽然它的运行时间可能是运行fact的三倍,但我们关心的是一般的增长顺序;或者这些过程的计算需求如何随着问题规模的变化而变化,并且两者在这种变化中都是线性的。

        既然我们在阶乘实现的过程中发现有迭代和递归两种方法,并且上面这种实现pascal的计算过程也是递归的,那么我们将采取迭代的方法实现pascal来进行对比,以期达到更好的性能。代码如下:

(define pascal-3
    (lambda (j n)
       (/ (help(n 1 (+ n (- j) 1)) (help j 1 1)))
))

(define help 
    (lambda (k prod end)
       (if (= k end) (* k prod)
           (help (- k 1) (* prod k) end)
)))

        我们的意思是只计算两个乘积:一个用于分子,一个用于分母,并保存我们在对阶乘的额外调用中所做的额外乘法。毕竟,我们只是计算我们知道要分解的项的乘积。

        对这个版本做同样的分析。我们的help程序在空间上显然是恒定的,在时间上是线性的。我们的 Pascal 版本使用了它两次,因此虽然它比以前的版本花费的实际时间更少,但它仍然具有相同的一般行为:占用时间线性和占用空间不变。因此,实际上,这可能是最好的版本。

        但是理论上它与我们之前的版本具有相同的行为类别。这引出了我们的结论。首先,我们强调同一问题可能有许多不同的解决方案,并且这些解决方案可能具有非常不同的计算行为。有些问题本质上是指数级的。其他人可能有具有指数行为的简单解决方案,但通常一些额外的想法可以带来更有效的解决方案。我们的部分目标是让您认识不同类别的行为,并学习如何使用标准算法的属性来帮助您设计新问题的有效解决方案。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值