Chapter 1 构造过程抽象 [《SICP》 笔记]

来源:互联网 发布:公司财务报表软件 编辑:程序博客网 时间:2024/06/03 15:57

构造过程抽象

    1. 编写程序(procedure)指导过程(process)的进行
    2. 设计良好的计算系统,具有某种模块化的设计,各部分能独立构造、替换、排除错误。
  1. Lisp
    1. 表处理list processing,设计它是为了提供符号计算的能力,如微积分;
    2. 处理一种特定形式的逻辑表达式(递归方程)
    3. 最重要的一个特征是:process的Lisp描述(称为procedure)本身又可以做为Lisp的数据了表示和操作
      1. 填平"被动的"数据 和 "主动的"过程 之间的传统划分
  1. 程序设计的基本元素
    1. 程序设计语言不仅指挥计算机,更是一种框架,让人在其中组织思想
    2. 都有将简单认识组织成更复杂认识的方法,3种机制:
      1. 基本表达式,最简单的个体
      2. 组合的方法,从简单东西出发构造出复合的元素
      3. 抽象的方法,给复合对象命名,当作单元去操作
    3. 程序设计中,处理两类要素:过程和数据(并不严格分离);简单说,数据是操作的"东西",而过程是操作数据的规则的描述
    4. 对数据和过程都要提供上述3中机制
    1. 表达式
      1. 基本表达式
        1. 456
      2. 组合式
        1. (+ 123 345)
        2. 一对括号括起的一个表,最左运算符,其余是运算对象
        3. 求值就是将运算符表示的过程 用于运算对象的值(实际参数)
      3. 前缀表示
        1. 优点,使用可带任意个实参的过程,而没有歧义
        2. 优点,易嵌套,允许组合式元素本身又是组合式
    2. 命名和环境
      1. 命名
        1. (define var 2)
        2. 名字标识符称为变量,它的值为它所对应的对象
        3. define是最简单的抽象方法,允许用简单名字去引用 组合运算的结果
        4. 实际上,构造程序,就是逐步创建出越来越复杂的计算性对象;解释器提供这种便利,并促进采用递增方式去开发调试
      2. 环境
        1. 将值与符号关联,解释器必须维护名字-值对偶;这种存储就成为环境;一个计算过程中可能有多个不同环境
    3. 组合式的求值
      1. 组合式一般性求值规则
        1. 求值各子表达式;
        2. 将最左子式的值,应用于所有其他子式的值
      2. 递归,树形积累
        1. 这一规则,是递归的,有调用规则本身的需要
        2. 可用一棵树来表示求值过程,结点为组合式,发出分支对应子式;求值就是从端点向上穿行组合,进行"树形积累"(每个节点都把孩子收到一起)
        3. 反复用步骤一,直到不再遇到组合式而是基本表达式,取出数和名字的值
      3. 特殊形式
        1. 如define((define var 2)不是组合式),有自己的求值规则,这些不同的表达式(及相应求职规则)组成语法形式
      4. 复合过程
        1. 过程定义的一般形式:(define ( ) )
        2. 过程定义,强大的抽象技术,为复合操作提供名字,以作为单元使用,然后使用就与基本过程完全一样。
      5. 过程应用的代换模型
        1. 过程应用的代换模型,实际是一种计算过程,可用于确定过程应用的"意义"
        2. 只是帮助人领会,并非解释器实际工作方式;实际中不会去操作过程正文,去代换;而是利用局部环境,产生"代换"的效果。
        3. 只是第一个简单模型,后面会不适合,还会有更多精化的模型
      6. 应用序和正则序
        1. 除 前面讲到的求值规则(应用序),还有正则序求值,用表达式去代换形参,直到只剩基本式再计算求值;完全展开后再归约。
        2. 前面讲到的求值规则 成为 应用序求值,解释器实际采用的,先求值参数而后应用
        3. 能用替换模拟的,两序结果相同;应用序可避免重复计算,且当替换模型不可用时,正则序很麻烦,所以一般用应用序
    4. 条件表达式和谓词
      1. cond,if
        1. (cond ( ) ( )...),依次看谓词pi,直到某谓词为真
        2. else特殊符号,用在cond最后一句,永远返回,即else后谓词永为真
        3. if是cond只有两情况的特例;形式(if )
      2. 逻辑符合运算符
        1. and,or,not
    5. 练习
      1. ex1_1:略
      2. ex1_2:略
      3. ex1_3:略
      4. ex1_4行为是:a+|b|
      5. ex1_5若是应用序会死循环,反复不断求表达式(p); 若是正则序,结果0,代换所有表达式,(p)还没有求就结束了。
    6. 实例:采用牛顿法求平方根
      1. 说明与行动,数学函数与计算过程(有趣的观点)
        1. 平方根函数定义:根号x = y,使得y>=0且y^2=x,正统的数学函数,能推导出一些关于平方根的一般性事实;但并没有描述计算过程,给一个x,如何找到y
        2. 数学函数与计算过程的差异是说明性知识 与 行动性知识 间的普遍性差异的一个具体体现
        3. 数学关心说明性的描述(是什么),而CS关心行动性的描述(怎么做)
      2. 求平方根的牛顿法
        1. 已有猜测y, 新猜测为 y 与 x/y 的平均值,该值会更接近实际的平方根?如何证明啊?
      3. 练习
        1. ex1_6:if,cond都是特殊形式,与一般求值规则不同;定义的new-if没有短路性,所有子表达式都会计算,而死循环。
        2. ex1_7:对很大和很小的y,good-enough?不用固定差值了,而用相对值,用变化和猜测值的比率(绝对值和相对值)
        3. ex1_8:略
    7. 过程作为黑箱抽象
      1. 过程抽象
        1. sqrt程序可被分为多个过程,每个有自己明确的工作,且可被其他过程使用
        2. 像square与其说是一个过程,不如说是一个过程抽象;任何能计算出平方的过程都可用
        3. 一个过程定义需要能隐藏起一些细节,能被当作黑箱使用,不用弄清其中的具体实现。
      2. 局部名
        1. 约束变量,形式参数的名字,具体是什么不重要,只要在体内统一,体为其作用域
        2. 自由变量,非约束变量
      3. 内部定义和块结构
        1. 对用户由暴露必要的过程,其他都定义到该过程内部;这种嵌套定义,称为块结构,解决名字包装(减少冲突)问题
        2. 部分参数传递就不必要了,可以改为内部定义中的自由变量,这种方式称词法作用域(省去很多参数传递)
  2. 过程与它们产生的计算
    1. 知道一些基本语法,但不知常见模式,就像下棋只知移动规则,却不知典型开局,战术和策略;需要各种棋步的价值(值得定义那些过程)的知识,需要对后果(执行过程效果)做出预期的经验;只有能visualize 各种 procedures后,才能成为专家。
    2. 一个过程也就是一种模式,描述the local evalution of a computational process,即specifies how each stage of 程序 is built upon the previous stage.我们想评价整个程序whose local evolution 由 procedure specify。
    1. 线性的递归和迭代
      1. 以求n!为例
      1. 相同 of 两processes
        1. 可用substitution model 展开;计算步骤数都正比于n;求积的次序1,2,3...也是一样的
      2. 不同的"shape" of two processes,evolve很不一样
        1. recursive process,由代换模型看出,是先扩展,再计算收缩;扩展时建立了a chain of deferred operations;chain的长度正比于n(因此称线性);解释器需要维护额外信息,而不是仅靠程序变量,chain越长需要维护的信息却多
        2. iterative process,不grow和shrink,任何时刻的state仅由a fixed number of state variables 来 summarized,以及一个更新变量规则,一个终止条件;暂停后,只需要给解释器那几个变量就可继续,不用其他信息(迭代给够要保持的状态变量,递归并没有在这种意义上保持这些状态)
      3. 区分recursive process 与 recursive procedure
        1. recursive procedure,语法上的,定义自己时又用了自己
        2. recursive process,模式,evolve,而不是 the syntax of how a procedure is written,它会消耗n正比的mem
        3. 而scheme无此缺陷,只要是iterative process 一定常数空间,即使描述为recursive procedure;利用 tail-recursive,使常规过程调用能express迭代
      4. 练习
        1. ex1_9分辨是递归还是迭代,可用代换模型,也可直接看"自己"过程是否被"最后"调用,若是就是迭代,不会有没计算的操作(完全和循环又跳转到开头一样)
        2. ex1_10(A 0 n) (A 1 n) (A 2 n)这几个要依次求,求后面的会用到前面的,也算建立抽象了,否则很难算出;另外(A 3 n)类似4维空间一样,不能直观描述了,手上的数学工具不便描述
    2. 树形递归 tree recursion
      1. Fib(n)
        1. 直接根据定义来,很易写,但prcess的步数随n指数增长,而所占空间正比于n(指栈的最大深度,一般树形递归的步数都是指数,而空间线性)
        2. iterative process,用变量a,b,对应初态Fib(1),Fib(0),然后不断更新;计算步数与n成线性(就是动态规划)
      2. 有几种找零方式
        1. 定义为recursive procedure,为tree-recursive processes 易于理解和设计;划分为两组,包含某一零钱或不包含,树形展开,问题规模不断变小,直到变成degenerate cases。
      3. 练习
        1. ex1_11为写成iterative的形式,需要增加几个变量,同时让fz_iter"最后算"
        2. ex1_12打印出帕斯卡三角,下行元素是上行两个之和(写递归,关键是递归关系,=右侧看作已知量,定义中对自身的调用看作变量;然后接是返回条件)
        3. ex1_13...关于Fib(n)的值,未证...
    3. 增长的阶
      1. n,R(n)
        1. n是对the size of the problem的度量,R(n)是process所需资源
        2. n和R(n)可对应各种东西,如时间,内存等
      2. R(n)=Θ(f(n)),读作"theta of f(n)"
        1. k1*f(n) <= R(n) <= k2*f(n)
        2. 增长的阶只是粗略描述,但同时又提供了 a useful indication of 问题 size 改变对process行为的影响
      3. 练习
        1. ex1_14:空间O(n),因为每次存根到当前处的路?时间是指数吧,还是和其他什么有关?
        2. ex1_15:略
    4. 求幂exponentiation
      1. b^n
        1. 利用b^n=b*b^(n-1),且b^0=1,若写为recursive process,则时空消耗都为Θ(n);若写为iterative process,则时为Θ(n),空变为Θ(1).
        2. 而若利用b^n=(b^(n/2))^2,当n为偶;b^n=b*b^(n-1),当n为奇,则可得O(lgn)
      2. 练习
        1. ex1_16 a*b^n=a((b^2)^(n/2)); a*b^n=b^(n-1)*b*a=b^(n-1)*A; b^0 = 1; 参数a,b就是状态(ab^n为不变量,不变量,设计迭代算法的有力工具)
        2. ex1_17 完全类比fast-expt
        3. ex1_18 结合1.16和1.17,a*b+acc,初始acc=0,最后返回acc
        4. ex1_19 建立a2,b2与a0,b0的关系,然后对比形式就得p',q'与p,q的关系了(Fib(n+1)可看作对(0,1)的T^n次变换得(Fib(n),Fib(n+1)),可用连续平方变换去求T^n(注意T是一个变换;不仅数,过程也可用连续平方来思考)
    5. 最大公约数
      1. Euclid's Algorithm辗转相除 a=bc+r
        1. 若a/b = c...r,那么GCD(a,b)=GCD(b,r);因此可以不断变小问题,直到r=0
        2. 增长阶为Θ(logn),基于lame's定理得到增长阶(对定理就那么一提)
      2. 练习
        1. ex1_20 代换进去,应用序4次,正则序7次
    6. 素数检测
      1. 搜索因子
        1. 从[2,sqrt(n)]搜索检测因子,若无则是素数
          1. 取sqrt(n)终止的原因是,设n的因子的,则另一因子为n/d,其中必有一个<=sqrt(n),否则两因子的积>n.
      2. 费马检测
        1. 费马小定理:若n是素数,a是任一<n的整数,则a^n与a同余
        2. 外加一补充,若n不是素数,大部分a将不满足同余关系
        3. 因此得到算法,随机选a,判定,如不满足一定不是素数;若满足,则很可能是素数
      3. Probabilistic methods
        1. Fermat test与之前的算法不同,不保证答案正确,而是 probably correct.
        2. 错误来自两方面
          1. 未全检测
          2. 存在不是素数但满足性质的数
        3. 但同时Fermat test 随检测次数(尝试的a的个数)增加,可让错误的概率减小到任意程度
      4. 练习
        1. ex1_21 直接调用运行完事
        2. ex1_22 runtime返回微秒时间;还有newline和dispaly;通过实际运行时间验证增长率(这种方法也许有时可用)
        3. ex1_23 测试了2就不用再测试其他的偶数了,可以使运行速度减半(小技巧大幅提升速度)
        4. ex1_24 验证费马检测的增长率,确实很快
        5. ex1_25 expmod利用了 xmodm * ymodm = (x*y)modm,避免了溢出和对很大的数的乘法,对任意精度乘数太大会变慢(很重要和常用的方法)
        6. ex1_26 用*需要两个因子,虽然两因子相同但是都会算(注意消除不必要的计算)
        7. ex1_27 略
        8. ex1_28 Miller-Rabin不会被欺骗,对Fermat test的改进
  3. 用高阶函数做抽象
    1. (* x x x)能算立方,但却不能表述立方这一概念;需要能为公共的模式命名,建立抽象,而后直接在抽象的层次上工作;过程提供了这种能力。
    2. 高阶过程(procedure),以procedures作为参数,或者返回procedures。若限制为数字有局限性,因为有些相同模式使用不同过程。高阶表达这种概念。例如,∑f(n)中的f(n)为参数,∑为高阶过程
    1. 过程作为参数
        1. 类似 ∑f(n) = f(a)+...+f(b),不仅a,b是参数f也是参数
        2. 在有了以f作为参数的sum之后,就可use it a building block in formulating further concept.比如求定积分
      1. 练习
        1. ex1_29 用辛普森规则求定积分,公式变了,但还是可以看做sum形式(抽象模式,要从已有的模式出发思考)
        2. ex1_30 把递归的sum变成迭代的sum,有个result一直在传递,且最后返回这个result
        3. ex1_31 定义product,类似sum,只是+变成*;然后将product应用于计算pi的近似式(注意这种分层抽象,习惯这种抽象;还有自顶向下和自底向上思考)
        4. ex1_32 product是一种更一般的模式accumulate的特例,以+,*为参数
        5. ex1_33 给accumulate增加概念filter(问题空间中思考,概念上的分离,抽象能力)
    2. 用labmda构造过程
      1. lambda
        1. 形式(lambda ( )
        2. 与define create procedure 的方式一样,只是lambda没有名字,即区别只是 the result procedure 在环境中没有名字
      2. let
        1. 有时需要局部变量,可以用define建立一个辅助过程来约束这些变量;还以用lambda建匿名过程,局部变量在匿名过程的参数上
        2. 还以用特殊形式let,其一般形式为 (let (( <exp1)...( <expn)) )
        3. let仅仅是lambda的语法外衣
      3. 练习
        1. ex1_34 ?f是名字,其值对应相应的lambda?
    3. 过程作为一般性的方法
      1. 两种过程
        1. 复合过程(就是define),将若干操作的模式抽象出来,使描述的计算不再依赖特定的数值.
        2. 高阶过程,表述计算的一般性过程,与特定函数无关
      2. 区间折半求方程的根
        1. 求f(x)=0的根,利用正负之间必有0点,不断试解,并根据符号缩小范围
        2. 运行时间Θ(log(L/T)),其中L是区间长度,T是误差范围(感觉L/T就是离散化)
      3. 找出函数的不动点
        1. 不动点f(x)=x,通过f(x), f(f(x)), f(f(f(x)))... 直到变化不大,只对某些函数适用,例如,对sin(x)=x能正常工作
        2. 用来求平方根
          1. 解y^2=x, 给定x, 有 y=x/y, 即f(y)=x/y=y;
          2. 用上述方法,直接求不动点,会不收敛而无限循环;
          3. 解决办法,不动点在y1与x/y1之间,猜个中间1/2*(y1+x/y1),即把y=x/y,变成求y=1/2(y+x/y)(恒等式变形,就解决问题)
          4. 这种用平均值去逼近一个解的方法陈伟平均阻尼(就是控制震荡,让猜测变化不太剧烈)
      4. 练习
        1. ex1_35 黄金分割点就是 x^2= x+1的解,恒等式就证明了;直接带入函数计算
        2. ex1_36 平均阻尼快些,不用阻尼小有明显震荡
        3. ex1_37 (a)大概k=13; (b)递归从i算到k,而迭代需要从k算到1才行(改成迭代还要改变计算顺序)
        4. ex1_38 就是一样求连除式
        5. ex1_39 同上
    4. Procedures as Return Values
      1. 平方根
        1. 可将不动点搜索,平均阻尼,函数y=x/y,这3种思想结合到一起来求平方根
        2. 即,(fixed-point (average-dump (lambda (y) (/ x y))) 1.0)
        3. 这样表述出来的procedure 与前面没有划分的会产生同一个process; 一般一个process可变为多种prcedures, 而有经验的程序员能设计出好的procedure, 清晰的易理解, process能分为有用的相互分离的个体,使这些个体能用于别处。(模块化)
      2. 牛顿法
        1. (fixed-point (newton-transfor g) 1.0), 将求g(x)=0的解,转化为求 x= x - g(x)/Dg(x)的不动点
      3. 抽象和第一级抽象
        1. 实际上上面是求平方根的两种不动点形式,每种都是从一个函数出发g, 然后变换产生f, 接着去找f的不动点,因此可以统一出一种模式
          1. 程序员应alert to opportunities to identify the underlying abstractions in our programs 然后build upon them 接着 generalize them 以 create more powerful abstraction
          2. 能这样抽象,就为将概念用于其它地方做好了准备;但也并不是要尽量抽象,而是要适度选择level.
          3. 高阶过程使能把这些抽象显示表示为programing langurage 中的基本元素,就像其他可计算元素一样
        2. 第一级元素,使用方式限制最少的元素
          1. 他们可以用变量命名,作为过程参数和返回,可包含在数据结构中;
          2. Lisp不同于很多语言,它给了过程完全的第一级状态
      4. 练习
        1. ex1_40 解3次方程的0点
        2. ex1_41 double连续k次,会调f 2^k次
        3. ex1_42 实现复合函数compose,将f和g合并为f(g(x))
        4. ex1_43 repeated
        5. ex1_44 平滑,多次平滑
        6. ex1_45 n次根 与 平均阻尼次数问题
        7. ex1_46 高阶过程,本章的数值算法都是迭代式改进
0 0