SICP随笔

来源:互联网 发布:php获取get数据 编辑:程序博客网 时间:2024/06/01 07:55

读一点想一点写一点
http://www.math.pku.edu.cn/teachers/qiuzy/progtech/
http://sicp.readthedocs.io/en/latest/

第一章:构造过程抽象

程序设计的基本元素

任何程序设计语言都必须能够表达数据过程

  • 表达式是Scheme的基本元素,解释器可对其直接求值
  • 括号括起来的组合式称为,表示过程调用
  • 表的最左元素是运算符,其余元素为其参数
  • “组合式”与“表”是同义词,每个表都可通过求值得到返回值
  • define是最简单的抽象手段,它建立名字到对象的映射

由于存在名字到实际对象的映射关系,解释器必须维护环境

对组合式求值的步骤是递归的,有两种顺序:

  • 正则序:先进行子表代换,最后进行求值。可以视为一种“深度优先”的求值策略。
  • 应用序:先对子表求值,然后调用过程。可以视为一种“广度优先”的求值策略。

应用序可以避免重复求值。

求值顺序会影响某些表达式的结果。

返回布尔值的过程称为谓词andornot三个谓词组成完备集,其中andor支持多于两个参数,并采用惰性求值策略。这意味着andor是Scheme的固有语法,而not是普通过程。condif是另外两个固有过程,均采用惰性求值。

实例:牛顿迭代法

牛顿迭代法是方程数值解法的一种。利用自顶向下的方法,写出牛顿法求平方根的算法如下:

(define (sqrt-iter guess x)  (if (good-enough? guess x)      guess      (sqrt-iter (improve guess x) x)))(define (improve guess x)  (average guess (/ x guess)))(define (average x y)  (/ (+ x y) 2))(define (good-enough? guess x)  (< (abs (- (square guess) x)) 0.0001))(define (square x) (* x x))(sqrt-iter 1.0 2.0)

这个例子展示了①通过递归可实现循环迭代;②过程作为“黑箱”的性质——用户不需要关心其内部实现,只要两个黑箱在相同的输入下有相同的输出,即认为黑箱是(外特性)等效的。借由过程封装,可实现程序结构的层次化。

Ex1.3 求最大和

现在需要定义一个过程(max-sum x y z),该过程接受3个参数,并返回最大的两个数之和。

传统的方法很无聊,所以深入思考一下:三个数只有三个和,这就是说,只需要找到最小的那个数就好了。

如果假定最左边的参数x是最小的,那么max-sum过程就是这样的:

(define (max-sum x y z)  (if (and (<= x y) (<= x z))      (+ y z)      (...)))

如果y或者z是最小的呢?完全可以再套两层if。但上面加粗的“最左边”三个字暗示着:这个最小值只与它在参数列表的位置有关。所以可以将整个参数列表左移,以观察下一个数是不是最小的。继续这个左移过程,总会遇到一个数是最小的。因此,省略号的位置就可以写成

(max-sum y z x)

这是一个递归调用。这个例子暗示(或者明示?)着选择结构、循环和递归的同构关系。在Scheme中,递归形式显然更优雅。(补充:虽然形式上是递归形式,但本质上是迭代的。参见Ex1.11。)

除此之外,这个例子也暗示着参数名称是无关紧要的这一规则。

这个方法非常机智,但并不是自己想出来的,而是受到启发才学到的(原始链接找不到了……以后补上),特此记录。还有,据说中文书中翻译有误,题目要求的是最大平方和而不是中文书的最大和,但这不重要,懒得改了。

Ex1.4 作为数据的运算符

(define (a-plus-abs-b a b)  ((if (> b 0) + -) a b))

这体现了“程序即数据”的思想。

Ex1.5 检测求值顺序

为检测解释器的求值顺序,执行以下代码:

(define (p) (p))(define (test x y)  (if (= x 0)      0      y))(test 0 (p))

如果采用正则序求值(即先代换展开,最后求值规约),或者应用序求值(每一步代换后都立即求值可求值部分),则执行效果如何?

首先可注意到p是一个很奇怪的存在:它被定义为它自身。这会导致每次规约p都会得到p本身。

如果采用正则序,则求值步骤如下:

;原表达式(test 0 (p));代换(if (= 0 0) 0 (p));直接可得到第一分支0

但如果采用应用序:

;原表达式(test 0 (p));对可规约的(p)进行规约(test 0 (p));又回来了

陷入了无尽的代换。实际执行就会发现,真的卡住了,说明Scheme是采用应用序求值。

Ex1.6 if作为固定语法的必要性

(define (new-if p a b)  (cond (p a)        (else b)))(define (sqrt-iter guess x)  (new-if (good-enough? guess x)          guess          (sqrt-iter (improve guess x) x)))(sqrt-iter 1.0 2.0)

(sqrt-iter 1.0 2.0)进行求值,采用默认的应用序,则求值过程为:

1. (sqrt-iter 1 2)2. (new-if (good-enough? 1 2)           1           (sqrt-iter (improve 1 2) 2))3. (new-if #x           1           (sqrt-iter 1.5 2))4. (new-if #x           1           (new-if (good-enough? 1.5 2)                   1.5                   (sqrt-iter (improve 1.5 2) 2)))5. (new-if #x           1           (new-if #x                   1.5                   (sqrt-iter 1.4167 2)))6. ...

可见,由于应用序求值总是倾向于规约所有可求值子表,因此每轮求值都会不顾谓词的取值而强行展开所有子句。如果使用正则序,则结果更加显然:每轮求值都是不顾谓词的取值而代换掉new-if,而new-if展开式中重复出现sqrt-iter,导致这个过程无穷无尽。

问题的根源在于if的求值顺序依赖于谓词子句的取值,Scheme将其作为固定语法,不受默认求值顺序的束缚。这些不按套路出牌的特殊形式,可以说是一门语言的基石,在众生平等的函数式编程世界,也需要有一些特殊的规则。

The Little Schemer里面引用了乔治·奥威尔的一句话:

All animals are equal, but some animals are more equal than others.

就是这个道理。

Ex1.9 迭代与递归

现有两种实现加法函数的方式:

;递归的(define (plus-r a b)  (if (= a 0)      b      (inc (plus-r (dec a) b))));迭代的(define (plus-i a b)  (if (= a 0)      b      (plus-i (dec a) (inc b))))

展开可以发现两者的特性:plus-r延缓了inc的求值,plus-i则每次递归调用时都会修改ab两个参数。

根据以往对Church encoding的了解,所谓的加法,其实可以理解为“+1”的多次迭代,这与对乘法和乘方等“高阶”运算的理解是类似的。“+1”实际上就是皮亚诺公理的“后继”操作。

Ex1.10 Ackermann函数

这个东西蛮无聊的,最后总结起来就是乘2、2的幂,以及超幂,等等。

树形递归

最典型的就是斐波那契数列:

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

容易改写成迭代方式。

但是有一类问题,它们的子问题与问题本身遵循相同的求解规则,这类问题就适合采用递归的方式去解决。问题:现有足够多的1、5、10、25、50五种面值的硬币,为了将100换成零钱,有多少种可能的组合方案?

按照生活习惯,一般都是从最大面值的零钱开始换起,这也是为达到零钱数最少的局部最优结果而采取的贪心策略。实际上,100元换零钱的方式中,可以分为“有x元”的方案和“没有x元”的方案,并且容易知道,总的方式数等于不用x元的方案总数加上用全部种类的硬币换剩余( 100 - x )元的方案总数。如果换0元钱,视为有1种方案;如果硬币种类数为0,视为有0种方案。

记:

  • 现金总数为a(amount)
  • 使用的硬币种类数为k(kinds-of-coins)
  • k种硬币最大的硬币面值(max k),事实上可以不是最大面值,为理解方便,定为最大面值
  • 使用k种硬币组合成amount元现金的方案数为(N k a)

则可以记约定如下:

  • (N 0 a) = 0,即用0种硬币的找零方案为0种;
  • (N k 0) = 1,即0元的兑换方案为1种;
  • (N k <负数>) = 0
  • (N 1 a) = 1,即只用1元换任何数额的零钱都只有一种方案。

并且知道:

;用不规范的写法(N k a) = (N (k-1) a) + (N k (a-(max k)))

至此立刻可以用Scheme写出N的定义:

(define (N k a)  (cond ((= a 0) 1)        ((or (< a 0) (= k 0)) 0)        (else (+ (N (- k 1) a)                 (N k (- a (max k)))))))(define (max k)  (cond ((= k 1) 1)        ((= k 2) 5)        ((= k 3) 10)        ((= k 4) 25)        ((= k 5) 50)))(N 5 100)

实际运行结果是292,此为正确答案。

但是希望采用迭代的方式解决此问题,为此,考察(N k a)的求值规律。不妨以方便的形式将N记为N(k,a),尝试根据上面的递推式求几个值:

k=1 {1} k=2 {1,5} k=3 {1,5,10} N(1,1)=1 N(2,1)=N(1,1)+N(2,(1-5))=1+0=1 N(1,2)=1 N(2,2)=N(1,2)+N(2,(2-5))=1+0=1 N(1,3)=1 N(2,3)=N(1,3)+N(2,(3-5))=1+0=1 N(1,4)=1 N(2,4)=N(1,4)+N(2,(4-5))=1+0=1 N(1,5)=1 N(2,5)=N(1,5)+N(2,(5-5))=1+1=2 N(3,5)=N(2,5)+N(3,(5-10))=2+0=2 N(1,6)=1 N(2,6)=N(1,6)+N(2,(6-5))=1+1=2 N(1,10)=1 N(2,10)=N(1,10)+N(2,(10-5))=1+2=3 N(3,10)=N(2,10)+N(3,(10-10))=3+1=4

写了这些,大概就可以看出后面结果对前面结果的依赖。这反映了“子问题”与原始问题的同构性质,适合采取动态规划的算法迭代地解决。

package coin;public class Coin {    public static void main(String[] args) {        System.out.println(count_change(100));    }    public static int count_change(int n) {        int[] A = new int[n+1];        int[] max = {1, 5, 10, 25, 50};        A[0] = 1;        for (int j = 1; j <= n; j++)            A[j] = 0;        for (int i = 0; i < 5; i++)        {            for (int j = max[i]; j <= n; j++)            {                A[j] += A[j - max[i]];            }        }        return A[n];    }}

Ex1.11 递归风格与迭代风格的转换

受书中Fib递归写法的启发,解决了这道题。Scheme代码如下:

;递归(define (f-r n)  (cond ((< n 3) n)        (else (+ (f-r (- n 1)) (* 2 (f-r (- n 2))) (* 3 (f-r (- n 3)))))));迭代(define (f-i n s1 s2 s3)  (cond ((= n 0) s1)        (else (f-i (- n 1) s2 s3 (+ (* 3 s1) (* 2 s2) s3)))));逐个打印(define (show n)  (display (f-r n))  (display "    ")  (display (f-i n 0 1 2))  (newline)  (if (= n 0)      (display "Done!")      (show (- n 1))))(display "recur")(display "  ")(display "iter")(newline)(show 15)

输出结果:

recur   iter142717  14271760104   6010425315   2531510661   106614489    44891892    1892796 796335 335142 14259  5925  2511  114   42   21   10   0Done!

套路总结:这类问题总是能隐约嗅到“卷积”“时域平移”的味道。系统每一时刻的状态都由以前的状态决定。这类系统好像有个名字来着,忘记了。反正就是,在操作上,有非常明显的“平移”特征。如果以寄存器的思维方式来思考这个问题,由于系统的状态与之前三个时点的状态有关,所以需要三个寄存器来存储这三个过去状态。计算下一个时点的状态时,只需要利用这个三寄存器的窗口就足够了。画个表格来说明这个问题:

n 0 1 2 3 4 5 … s1 0 1 2 4 11 25 … s2 1 2 4 11 25 59 … s3 2 4 11 25 59 142 …

其中s3是从n=1开始依据上一窗口计算出来的最新状态。观察序列可知,s1即为f(n)。

更加概括地说,系统每一时点的状态都依赖于前三个时点的状态,因此①任一时点必须存储3个时点宽度的过往状态;②每一时点的状态实际上是由之前所有状态决定的。因此,需要迭代地计算从f(0)到f(n-1)的所有值。在Scheme中,迭代体现为递归形式。

Ex1.12 杨辉三角

该题要求读者“计算”出杨辉三角,并没有显式地要求以某种格式输出之。

杨辉三角可以说是常识了。关键问题不在杨辉三角,而在于如何将杨辉三角美观地打印在命令行窗口中。

(define (yanghui row col)  (cond ((= col 0) 1)        ((> col row) 0)        ((<= col row) (+ (yanghui (- row 1) (- col 1)) (yanghui (- row 1) col)))))(define (triangle r c)  (display (yanghui r c))  (display "    ")  (cond ((< r c) (display "illegal arg"))        ((= r 0) (newline))        ((= c 0) (newline)(triangle (- r 1) (- r 1)) )        (else (triangle r (- c 1)))))(triangle 10 10)

执行后输出

1   10  45  120 210 252 210 120 45  10  1   1   9   36  84  126 126 84  36  9   1   1   8   28  56  70  56  28  8   1   1   7   21  35  35  21  7   1   1   6   15  20  15  6   1   1   5   10  10  5   1   1   4   6   4   1   1   3   3   1   1   2   1   1   1   1

这个递归形式表达的双层循环着实让我想了一会儿。多说一句:杨辉三角的每一项都是二项式系数,即组合数。组合数可以通过阶乘函数计算得到。据此可以迭代地求解出杨辉三角每一个元素,避免递归的低效。懒得做了。

Ex1.13 斐波那契与黄金分割

这个证明题利用数学归纳法很容易证明。题中gamma等于

γ=152

Ex1.14 换零钱的递归树

可见 http://sicp.readthedocs.io/en/latest/chp1/14.html 。

Ex1.16 乘方的快速迭代实现

基于
n为偶数:

bn=(bn/2)2=(b2)n/2

n为奇数:
bn=b(bn1)

设计快速迭代求幂算法如下:

(define (expf b n a)  (if (= n 0)      a      (if (even? n)          (expf (* b b) (/ n 2) a)          (expf b (- n 1) (* b a)))))

(expf b n a)的第一个参数用来保存底数(n为偶数时则平方),第二个参数用来保存迭代计数(即n),第三个参数作为乘积计数器,记录最终结果,其初始值为1。

从以上两式可见,只有当n为奇数时,才会向第三个参数累积之前计算的第一个参数。

最近一个是事情比较多,再一个是过于纠结习题。明天开始,尽量以主干内容为主,习题可以稍微放一放。
并且,应该读一读The Little Schemer

最大公约数GCD

经典的GCD欧几里得算法:

(define (gcd a b)  (if (= b 0)      a      (gcd b (remainder a b))))
原创粉丝点击