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
是最简单的抽象手段,它建立名字到对象的映射
由于存在名字到实际对象的映射关系,解释器必须维护环境。
对组合式求值的步骤是递归的,有两种顺序:
- 正则序:先进行子表代换,最后进行求值。可以视为一种“深度优先”的求值策略。
- 应用序:先对子表求值,然后调用过程。可以视为一种“广度优先”的求值策略。
应用序可以避免重复求值。
求值顺序会影响某些表达式的结果。
返回布尔值的过程称为谓词。and
、or
和not
三个谓词组成完备集,其中and
和or
支持多于两个参数,并采用惰性求值策略。这意味着and
和or
是Scheme的固有语法,而not
是普通过程。cond
和if
是另外两个固有过程,均采用惰性求值。
实例:牛顿迭代法
牛顿迭代法是方程数值解法的一种。利用自顶向下的方法,写出牛顿法求平方根的算法如下:
(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
则每次递归调用时都会修改a
和b
两个参数。
根据以往对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),尝试根据上面的递推式求几个值:
写了这些,大概就可以看出后面结果对前面结果的依赖。这反映了“子问题”与原始问题的同构性质,适合采取动态规划的算法迭代地解决。
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!
套路总结:这类问题总是能隐约嗅到“卷积”“时域平移”的味道。系统每一时刻的状态都由以前的状态决定。这类系统好像有个名字来着,忘记了。反正就是,在操作上,有非常明显的“平移”特征。如果以寄存器的思维方式来思考这个问题,由于系统的状态与之前三个时点的状态有关,所以需要三个寄存器来存储这三个过去状态。计算下一个时点的状态时,只需要利用这个三寄存器的窗口就足够了。画个表格来说明这个问题:
其中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等于
Ex1.14 换零钱的递归树
可见 http://sicp.readthedocs.io/en/latest/chp1/14.html 。
Ex1.16 乘方的快速迭代实现
基于
n为偶数:
n为奇数:
设计快速迭代求幂算法如下:
(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))))
- SICP随笔
- sicp
- SICP
- sicp-1
- SICP参考资料
- SICP习题
- SICP HASKELL
- SICP 1.7
- SICP 1.11
- SICP 第一章
- SICP list
- SICP HuffmanCode
- SICP PolySystem
- SICP banksystem
- SICP Montecarlo
- SICP deque
- SICP Table
- SICP 1.20
- SpringBoot整合Shiro
- SQL-用JOIN连接多个表
- 用Python函数实现求取一个正实数平方根的简单算法
- 回溯算法--收费公路重建问题
- 日志报错:java.lang.IlleagalArgumentExcption: entity not in the persistence context
- SICP随笔
- HTML5中的output标签
- HTML实现用户注册界面
- QGC的MainToolBarIndicator
- 1019(树状数组的区间更新,区间求和)
- U3D中模型后面加载2D背景(多相机分层显示 )
- HBase(1)-HBase的分布式安装
- JAVA获取远程文件
- RxAndroid and Kotlin (Part1)