《计算机程序的构造和解释》学习笔记

学习计划:总22节,每节1天,预计22天学习时间+4天练习时间,2013年4月6日~2013年5月2日

20130406==============================

Emacs中使用mit-scheme

安装好mit-scheme之后,虽然自带的edwin(类emacs编辑器)也不错了,但是缺少了语法高亮多少还是有点不方便。这里主要是讲如何在emacs里面使用mit-scheme的方法。

首先在~/.emacs里面加入如下的语句:

;;; Always do syntax highlighting  
(global-font-lock-mode 1)  
;;; Also highlight parens  
(setq show-paren-delay 0  
      show-paren-style 'parenthesis)  
(show-paren-mode 1)  
;;; This is the binary name of my scheme implementation  
(setq scheme-program-name "scm")

注意上面的最后一行里面的scm要修改成对应的scheme解释器的程序名。

然后在新启动的emacs里面如果打开后缀为.scm或者.ss的文件,那么默认是当成scheme的文件,并且开启了语法高亮。

如果需要在编辑的源代码里面调用scheme解释器的话,可以按以下的步骤来进行:

  1. C-x 2   ;;这个是用来新打开一个水平分割的窗口。
  2. C-x o   ;;跳转到这个新打开的窗口。
  3. M-x run-scheme  ;;在新打开的窗口里面运行scheme解释器。

现在你就可以像用edwin一样来使用嵌入了scheme的emacs了。下面两个key可以用来马上执行文件的语句:

  1. C-x C-e   ;;将光标之前的最后一个语句交给scheme解释并执行。
  2. C-x h C-c C-r  ;;将整个buffer的内容都交给scheme解释执行。


一、构造过程抽象

心智的活动,除了尽力产生各种简单的认识之外,主要表现在如下三个方面:

1)将若干简单认识组合为一个复合认识,由此产生出各种复杂的认识。

2)将两个认识放在一起对照,不管它们如何简单或复杂。在这样做时并不将它们合而为一,由此得到有关它们的相互关系的认识。

3)将有关认识与那些在实际中和它们同在的所有其他认识隔离开,这就是抽象,所有具有普遍性的认识都是这样得到的。

我们准备学习的是有关计算过程的知识


Lisp并不是一种主流语言,为什么要用它作为程序设计的基础呢?

因为它具有许多独立特征,这些特征使它成为研究重要程序的设计、构造,以及各种数据结构,并将其关联于支持它们的语言特征的一种极佳媒介。

比如最重要的特征:计算过程的Lisp描述本身又可以作为Lisp的数据来表示和操作。现存许多威力强大的程序设计技术,都依赖于填平在“被动的”数据和“主动的”过程之间的传统划分。然而Lisp具有可以将过程作为数据进行处理的灵活性,使它成为探索这些技术的最方便的现存语言之一。

1.1程序设计的基本元素

一种强有力的程序设计语言,不仅是一种指挥计算机执行任务的方式,还应该成为一种框架,使我们能够在其中组织自己有关计算过程的思想。这就需要具备最少以下三种机制:

1)基本表达形式,用于表示语言所关心的最简单的个体。

2)组合的方法,通过它们可以从较简单的东西出发构造出复合的元素。

3)抽象的方法,通过它们可以为复合对象命名,并将它们当作单元去操作。

1.1.1表达式

(+ 11 22)是一个表达式,其结果为33。

注意对于多重嵌套运算,应用格式良好的形式书写,这样有助于阅读。

()称为组合式,+是运算符,其余项11和22是运算对象

表达式是定义一个数据操作过程的最小单位,通常由运算符+运算对象(这两者在书中也被称为子表达式)组成的表达式称为组合式。

1.1.2命名和环境

(define size 2)定义了一个变量size=2。

(*2(+size 3))结果为10。

定义圆周长运算组合式:

(define pi 3.14159)

(define radius 10)

(define circumference (* 2 pi radius))

一般而言,计算得到的对象完全可以具有非常复杂的结构,如果每次需要使用它们时,都必须记住并重复地写出它们的细节,那将是极端不方便的事情。

实际上,构造一个复杂的程序,也就是为了去一步步地创建出越来越复杂的计算性对象。解释器使这种逐步的程序构造过程变得非常方便。

于是,一个Lisp程序通常总是由一大批相对简单的过程组成。

命名的意义在于,使我们能通过名字去使用(一些可能过程及其复杂)计算机对象(,而不必再去在新的过程中重新构建它)。
这些名字也可以称之为符号,在上例中可见,将值与符号关联,而后又提取这些值,意味着解释器必须维护某种存储能力,以便保持有关的名字-值对偶的轨迹,这种存储被称为环境

1.1.3组合式的求值

本章目标是把与过程性思维有关的各种问题隔离出来。以下是解释器的工作过程:

1)求值该组合式的各个子表达式。(这一步骤是递归方式执行的)

2)将作为最左子表达式(运算符)的值的那个过程应用于相应的实际参数,所谓实际参数也就是其他子表达式(运算对象)的值。

例如:(* (+ 2(* 4 6))(+ 3 5 7))

我们可以采用一棵树的形式,用图形标示这一组合式的求值过程,其中的每个组合式用一个带分支的结点表示,由它发出的分支对应于组合式里的运算符和各个运算对象。(这一计算过程称为“树形累计”)

组合式的求值过程可以视作“树形积累”,每个子表达式(运算符与运算对象)都视为一个节点。


一般而言,我们应该把递归看做一种处理层次性结构的(像树这样的对象)极强有力的技术。

处理这些基础情况的方式如下规定:

1)数的值就是它们所表示的数值。

2)内部运算符的值就是能完成相应操作的机器指令序列。(这一规定可以看作是下一条规定的特殊情况)

3)其他名字(比如之前定义过的size、pi等)的值就是在环境中关联于这一名字的那个对象。

一般性求值规则的例外称为特殊形式,define是我们接触到第一个特殊形式,它不应用于除它以外的实际参数,而是将一个变量名关联到一个值。

1.1.4复合过程

Lisp里的部分元素必然会出现在任何一种强有力的程序设计语言里,包括:

1)数和算术运算是基本的数据和过程。

2)组合式的嵌套提供了一种组织起多个操作的方法。

3)定义是一种受限的抽象手段,它为名字关联相应的值。

复合过程的定义:(define (<name> <formal parameters>) <body>),eg:(define (square x)(* x x))

用复合过程定义另一个复合过程eg:(define (sum-of-squares x y)(+ (square x) (square y)))

复合过程与C语言中的函数定义类似,表达式1中最左侧的子表达式可以理解为是一种自定义过程的运算符,应用于除它以外的子表达式,表达式2中则申明了“自定义运算符”的求值规则。

1.1.5过程应用的代换模型

解释器的工作方式是对组合式中的各个元素(子表达式)求值,而后将得到的那个过程(组合式里运算符的值)应用于那些实际参数(组合式里运算对象的值)。

(define (square x)(* x x))
(define (sum-of-squares x y) (+ (square x)(square y)))
(define (f a)(sum-of-squares (+ a 1)(× a 2)))
(f 5)
将a的实际参数5代换f体的形式参数得:(sum-of-squares (+ 5 1)(× 5 2)),问题被归约为对另一组合式的求值。
进一步归约:(+ (square 6)(square 10))。
使用square的定义又可以将它归约为:(+ (× 6 6)(× 10 10))。
通过乘法又能将它进一步归约为:(+ 36 100),最后得到136。

上述这种计算过程称为过程应用的代换模型。

代换模型分:应用序求值(先求值参数而后应用) & 正则序求值(完全展开后求值),解释器实际使用的是应用序。

需要注意:代换的作用只是为了帮助我们领会过程调用中的情况,而不是对解释器实际工作方式的具体描述。

1.1.6条件表达式和谓词

计算x的绝对值,如果x>0取x,如果x=0取0,如果x<0取-x,这种结构称为一个分情况分析,Lisp中使用cond(conditional)来处理这一结构。

(define (abs x)(cond ((> x 0) x)((= x 0) 0)((< x 0) (-x))))

条件表达式的一般性形式:(cond (<p1> <e1>)(<p2> <e2>)…(<pn> <en>))

在每个对偶中的第一个表达式(即<p>)是一个谓词,它的值将被解释为真或假。

求值方式:若谓词p1被解释为false,就去进行谓词p2的求值,若仍为false,就循环的求p3、p4,直到某一个谓词得到true,就返回响应子句中的序列表达式<e>的值,视作整个条件表达式的值。若所有<p>都为false,则cond的值被视作没有定义。

绝对值表示法2:(define (abs x)(cond ((< x 0)(- x))(else x)))

绝对值表示法3:(define (abs x) (if (< x 0) (- x)  x)),if表达式的一般形式:(if <predicate> <consequent> <alternative>)

除了一批基本谓词如<、=、>之外,还有一些逻辑复合运算符,利用它们可以构造出各种复合谓词,常用的如(and <e1> ... <en>)(or<e1> ... <en>)(not <e>)

1.1.7实例:采用牛顿法求平方根

在数学的函数和计算机的过程之间有一个重要差异,那就是,这一过程必须是有效可行的。

比如求平方根可以描述为:平方根x=那样的y,使得y>=0而且y的平方=x

用Lisp的形式可以写为:(define (sqrt x) (the y (and (>= y 0) (= (square y) x))))

但这只不过是重新提出了原来的问题,函数与过程之间的矛盾,不过是在描述一件事情的特征,与描述如何去做这件事情之间的普遍性差异的一个具体反映。

因此,计算平方根仅靠描述是不够的,最常用的是牛顿的逐步逼近方法:如果对x的平方根的值有了一个猜测y,那么就可以通过执行一个简单操作去得到一个更好的猜测,只需要求出y和x/y的平均值(它更接近实际的平方根值)。

需要用到之前定义过的abs与square:
(define (square x) (* x x))
(define (abs x) (if (< x 0) (- x)  x))
(define (sqrt-iter guess x) (if (good-enough? guess x)  guess (sqrt-iter (improve guess x) x)))

改进1,求出它与被开方数除以上一个猜测的平均值:
(define (improve guess x) (average guess (/ x guess)))
其中
(define (average x y) (/ (+ x y) 2))

改进2,说明什么是“足够好”,这里采用的方法并不是一个很好的检测方法,误差值为0.001:
(define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001))

改进3,设定一种方式来启动整个工作,用1.0作为对任何数的初始猜测值:
(define (sqrt x) (sqrt-iter 1.0 x))

sqrt-iter展示了如何不用特殊的迭代结构来实现迭代,其中只需要使用常规的过程调用能力。

函数与过程之间的矛盾是说明性的知识(描述一件事)与行动性的知识(如何完成一件事)之间的差异。

1.1.8过程作为黑箱抽象

上一小节中的sqrt程序可以看作一族过程,它们直接反应了从原问题到子问题的分解(其中sqrt-iter的定义是递归的,1.2节中,将细致讨论)。

这一过程分解的意义在于,我们可以将一个大程序看作若干过程模块的集合,每个过程完成一件可以清楚标明的工作。
例如,当我们基于square定义过程good-enough?之时,就是将square看做一个“黑箱”(即无须关注square这个过程是如何计算出它的结果的)。
因此,如果只看good-enough?过程,与其说square是一个过程,不如说它是一个过程的抽象,即过程抽象

由此可见,一个过程定义应该能隐藏起一些细节。这使过程的使用者可能不必自己去写这些过程,而是从其他程序员那里作为一个黑箱而接受了它。

局部名:过程的形式参数名必须局部于有关的过程体,形式参数的名字称为约束变量,而非约束的称为自由的
一个完成的过程定义里的某个约束变量统一换名,并不影响这个过程定义的意义。
比如good-enough?的定义中,guess和x是约束变量,而<、-、abs、square是自由的。

内部定义和块结构:诸如上述定义(define (square……))(define (sqrt……)),sqrt使用了square等过程,square被定义为自由的,显然用户无法再次定义名为square的过程,因为这样会影响sqrt的执行结果,在许多程序员一起构造大系统的时候,这一问题将会变得非常严重。为了解决这一问题,可以将square定义在sqrt内部:
(define (sqrt x)
  (define (good-enough? guess x) (< (abs (- (square guess) x)) 0.001))
  (define (imporve guess x) (average guess (/ x guess)))
  (define (sqrt-iter guess x) (if (good-enough? guess x)  guess (sqrt-iter (improve guess x) x)))
(sqrt-iter 1.0 x))
这种嵌套的定义(内部定义的方式)称为块结构
因为x在sqrt的定义中是受约束的,因此我们还能简化它:
(define (sqrt x)
  (define (good-enough? guess) (< (abs (- (square guess) x)) 0.001))
  (define (improve guess) (average guess (/ x guess)))
  (define (sqrt-iter guess) (if (good-enough? guess)  guess (sqrt-iter (improve guess))))
(sqrt-iter 1.0))
这种方式称为词法作用域。

1.2过程与它们所产生的计算

能够看清所考虑的动作的后果的能力,对于成为程序设计专家是至关重要的,就像这种能力在所有综合性的创造性的活动中的作用一样。

一个过程也是一种模式,它描述了一个计算过程的局部演化方式,描述了这一计算过程中的每个步骤是怎样基于前面的步骤建立起来的。在有了一个刻画计算过程的过程描述之后,我们当然希望能做出一些有关这一计算过程的整体或全局行为的论断。一般来说这是非常困难的,但我们至少还是可以试着去描述过程演化的一些典型模式。

这一节,将考察一些简单过程所产生的计算过程的“形状”,还将研究这些计算过程消耗各种计算资源(时间和空间)的速率。

20130407==============================

1.2.1线性的递归和迭代

乘阶函数是大家熟悉的一种展开这一对比的很好的例子,n!=n*(n-1)*(n-2)*(n-3)...3*2*1,分别用方式1(递归)和方式2(迭代)来实现它。

方式1:(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1)))))


方式2:(define (factorial n ) (fact-iter 1 1 n))  (define (fact-iter product counter max-count) (if (> counter max-count) product (fact-iter (* counter product) (+ counter 1) max-count)))

通过应用替换模型(代换模型)的角度看,我们很容易发现两者“形状”上的区别。

方式1
形状:计算过程构造起一个推迟进行的操作所形成的链条(在这里是一个乘法的链条),收缩阶段表现为这些运算的实际执行。这种由一个推迟执行的运算链条刻画的计算过程称为递归计算过程
特点:解释器需要维护好那些以后将要执行的操作的轨迹。在计算阶乘n!时,推迟执行的乘法链条的长度也就是为保存其轨迹需要保存的信息量,这个长度随着n值而线性增长(正比于n)。这样的计算过程称为线性递归过程

方式2
形状:计算过程没有任何增长或收缩。对于任何一个n,在计算过程中的每一步,在我们需要保存的轨迹里,所有的东西就是变量product、counter和max-count的当前值。这种过程称为迭代计算过程
特点:其状态可以用固定数目的状态变量描述;存在着一套固定的规则,描述了计算过程在从一个状态到下一个状态的转换时,这些变量的更新方式;还有一个(可能有的)结束检测。这种计算步骤随n线性增长的过程称为线性迭代过程

从另一角度看,在迭代中,那几个程序变量提供了有关计算状态的完整描述,意味着无论在哪一步停下来,只要为解释器提供有关这三个变量的值就能唤醒计算。
而递归,还存在着另外一些“隐含”信息,它们并未保存在程序变量里,而是由解释器维持着,指明了过程在所推迟的运算形成的链条里"所处何处"。链条越长,需要保存的信息越多。

难点:区分递归计算过程的概念和递归过程的概念P23

1.2.2树形递归

另一种常见计算模式是树形递归,比如Fibonacci数序列的计算,规则:

将规则翻译为一个计算Fibonacci数的递归过程:

(define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2))))))

请注意,这里的每层分裂为两个分支(除了最下面),反映出对fib过程的每个调用中两次递归调用自身的事实。

上面的过程作为典型的树形递归具有教育意义,但它却是一种很糟的计算Fibonacci数的方法。因此Fib(n)值的增长相对于n是指数的。证明见P25

再用迭代方式计算:

(define (fib n) (fib-iter 1 0 n))  (define (fib-iter a b count) (if (= count 0) b (fib-iter (+ a b) a (- count 1))))

上述两种方式在资源消耗上差异巨大,树形递归的计算步骤=fib(n+1),而迭代的计算步骤=n。
但当我们考虑的是在层次结构性的数据上操作,而非对数操作时,树形递归就变成了一种自然的、强大的工具。

实例:换零钱方式的统计P26
(define (count-change amount) (cc amount 5))
(define (cc amount kinds-of-coins) (cond ((= amount 0) 1) ((or (< amount 0) (= kinds-of-coins 0)) 0) (else (+ (cc amount (- kinds-of-coins 1)) (cc (- amount (first-denomination kinds-of-coins)) kinds-of-coins)))))
(define (first-denomination kinds-of-coins) (cond((= kinds-of-coins 1) 1) ((= kinds-of-coins 2) 5) ((= kinds-of-coins 3) 10) ((= kinds-of-coins 4) 25) ((= kinds-of-coins 5) 50)))

1.2.3增长的阶

描述不同的计算过程在消耗计算资源的速率的差异上可用增长的阶的记法,它便于我们理解在输入变大时,某一计算过程所需资源的粗略度量情况。

20130408==============================

1.2.4求幂

求b的n次方的递归定义:
b^n=b*b^(n-1)
b^0=1

直接翻译为过程:
(define (expt b n) (if (= n 0) 1 (* b (expt b (- n 1)))))
之前以介绍过这种线性递归,需要Θ(n)步和Θ(n)空间。

然后再用迭代的方式重写:
(define (expt b n) (expt-iter b n 1))
(define (expt-iter b counter product) (if (= counter 0) product (expt-iter b (- counter 1) (* b product))))
线性迭代需要Θ(n)步和Θ(1)空间。

通过连续求平方得到更简化的版本,比如b^8用三次乘法算出它:
b^2=b*b
b^4=b^2*b^2
b^8=b^4*b^4
总结它的规律得:
b^n=(b^(n/2))^2   若n是偶数
b^n=b*b^(n-1)   若n是奇数
定义过程:
(define (fast-expt b n) (cond ((= n 0) 1) ((even? n) (square (fast-expt b (/ n 2)))) (else (* b (fast-expt b (- n 1))))))
(define (even? n) (= (remainder n 2) 0))
其中square是求平方的过程,remainder用于检测一个整数是否偶数。
显然在空间和步数上相对于n都是对数的,比如计算b^2n时,之需要比计算b^n多做一次乘法,每做一次新乘法,能够计算的指数值(大约)增大一倍。
连续求平方的线性递归过程需要Θ(logn)步和Θ(logn)空间。

随着n变大,Θ(logn)和Θ(n)双方的增长差异会变得非常明显,当然这不是最终版,还可以用线性迭代实现连续求平方得到更优的计算过程。

1.2.5最大公约数

两个整数a和b的最大公约数(GCD)定义为能除尽这两个数的那个最大的整数。当研究有理数算术的实现时,会需要GCD。

找出两个整数的GCD的一种方式是对它们做因数分解,并找出公因子。
但存在一种更高效的算法(欧几里德算法),基于以下观察:
GCD(a,b)=GCD(b,r)
如果r是a除以b的余数,那么a和b的公约数正好也是b的r的公约数,比如:
GCD(206,40)=GCD(40,6)=GCD(6,4)=GCD(4,2)=GCD(2,0)
将GCD(206,40)归约到GCD(2,0)最终得到2。可证,从任意两正整数开始,反复执行这种归约,最后产生出一个数对,当其中第二个数是0,另一个数就是GCD。

欧几里德算法过程化:
(define (gcd a b) (if (= b 0) a (gcd b (remainder a b))))
这将产生一个迭代计算过程,步数依所设计数的对数增长,这一事实与Fibonacci数之间有一种有趣关系:Lame定理P32

1.2.6实例:素数检测

1.3用高阶函数做抽象

通过之前的学习已经看到,在作用上,过程也是一种抽象,它们的描述了一些不依赖于特定数的复合操作。
例如,在定义(define (cube x) (* x x x))时,我们讨论的不是某个特定数值的立方,而是对任意数的立方。并且可以通过名字cude调用它。

人们对功能强大的程序设计语言有一个必然要求,就是能为公共的模式命名,建立抽象,而后直接在抽象的层次上工作。
过程提供了这种能力,这也是为什么除最简单的程序语言外,其他语言都包含定义过程的机制的原因。

然而,若将过程中参数限制为(只能为)数,会严重影响建立抽象的能力。因此经常有一些同样的设计模式能用于若干不同的过程。为了把这种模式描述为相应的概念,我们就需要构造出这样的过程,让它们以过程作为参数,或者以过程作为返回值。这类能操作过程的过程称为高阶过程。

20130409==============================

1.3.1过程作为参数

观察下面3个过程的共通性:

计算a到b的个整数之和
(define (sum-integers a b) (if (> a b) 0 (+ a (sum-integers (+ a 1) b))))

计算a到b范围内的整数的立方之和
(define (sum-cubes a b) (if (> a b) 0 (+ (cube a) (sum-cubes (+ a 1) b))))

计算1/(1*3)+1/(5*7)+1/(9*11)+...序列之和
(define (pi-sum a b) (if (> a b) 0 (+ (/ 1.0 (* a (+ a 2))) (pi-sum (+ a 4) b))))

可以看出这3个过程共享着一种公共的基础模式:用于从a算出需要加的项的函数,还有用于提供下一个a值的函数。可以总结出如下模板
(define (<name> a b) (if (> a b) 0 (+ (<term> a) (<name> (<next> a) b))))
数学家很早就认识到序列求和中的抽象模式,并提出了专门的“求和记法”,例如:

与此类似,作为程序模式,我们也希望能写出一个过程,去表述求和的概念,而不是只能写计算特定求和的过程。

1.3.2用lambda构造过程


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值