zoukankan      html  css  js  c++  java
  • SICP学习笔记(1.2.3 ~ 1.2.6)

                                                                SICP学习笔记(1.2.3 ~ 1.2.6)
                                                                            
    周银辉

    1.2.3到1.2.6实际是在讲“数据结构与算法”中的“时间复杂度”和“空间复杂度”,本来打算在这里将这两个话题串起来说说的,但整理了一下发现其会扩展到P/NP等诸多问题,所以打算将其放到以后的“算法导论学习笔记”的第 34章“NP完全性”中去说。这篇随笔主要说说这几节的练习题吧,蛮多的。

    1,练习1.14

    这个基于1.2.2中的“换零钱问题“,其在解法上是一个树形递归,SICP中说” In general, the number of steps required by a tree- recursive process will be proportional to the number of nodes in the tree, while the space required will be proportional to the maximum depth of the tree.“,很给人一个错觉是其时间复杂度和空间复杂度是线性的,不是的,“be proportional to”也可以是指数级增长嘛,实际上与求菲波拉涅数列类似的,其在空间复杂度上需要保存每个节点的值,所以为O (n),  而在时间复杂度上为指数级O(n^k),至于k的具体值,传说为5。

    2,练习1.15

    关于p被调用的次数,很简单,在调用p时设置一个计数器就可以了。所以运行一下下面的程序便可:

    (define count 0)
    (define (cube x) (* x x x))
    (define (p x) (- (* 3 x) (* 4 (cube x))))
    (define (sine angle)
       (if (not (> (abs angle) 0.1))
           angle
           (begin  (set! count (+ count 1)) (p (sine (/ angle 3.0))))))

    (sine 12.15)
    (display count)

    count的输出为 5
    而其时间复杂度为对数级

    3,练习1.16

    又是一个递归转尾递归的问题,按照“SICP学习笔记1.2.2” 中的做法,对于求幂 a^n,新的累计值= 旧的累计值 * a, 所以很容易得到下面的代码:

    (define (F a n)
      (G a n 1))

    (define (G a n counter)
        (cond ((= n 0) counter)           
              (else (G a (- n 1) (* a counter)))))

    当然,上面的代码是可以优化的,因为当 n 为偶数时 a^n = (a^2)^ (n/2) ,这样可以使时间复杂度成为对数级,所以上面的代码可以修改为:

    (define (F a n)
      (G a n 1))

    (define (G a n counter)
        (cond ((= n 0) counter)
              ((even? n) (G (* a a) (/ n 2) a))
              (else (G a (- n 1) (* a counter)))))


    4,练习1.17

    很简单,“依葫芦画瓢”就可以了:

    (define (double a) (+ a a))
    (define (halve a) (/ a 2))

    (define (F a b)
      (cond ((= b 0) 0)
            ((even? b) (double (F a (halve b))))
            (else (+ a (F a (- b 1))))))

    5,练习1.18

    即对练习1.17中的普通递归转尾递归:

    (define (double a) (+ a a))
    (define (halve a) (/ a 2))

    (define (F a b)
      (G a b 0))

    (define (G a b count)
      (cond ((= b 0) count)
            ((even? b) (double (G a (halve b) (halve count))))
            (else (G a (- b 1) (+ count a)))))

    6,练习1.19

    完整的程序是这样的:
    (define (fib n)
    (fib-iter 1 0 0 1 n))
    (define (fib-iter a b p q count)
      (cond ((= count 0) b)
            ((even? count)
             (fib-iter a
                   ;     b
                   ;     (+ (* p p) (* q q)) ; compute p'
                   ;     (+ (* q q) (* 2 p q)); compute q'
                   ;     (/ count 2)))
            (else (fib-iter (+ (* b q) (* a q) (* a p))
                   ;          (+ (* b p) (* a q))
                   ;          p
                   ;          q
                   ;          (- count 1)))))

    7,练习1.20

    应用序时,运行一下下面的代码就知道多少次了:

    (define (gdc a b)
      (if (= b 0)
          a
          (gdc b (begin (display "call remainder\n" )
                   ;      (remainder a b)))))

    (gdc 206 40)
    正则序嘛,由于解释器是不会这么干的,所以动铅笔和草稿纸吧,18次。

    8,练习1.21

    运行下面的程序:

    (define (prime? n)
      (= n (smallest-divisor n)))

    (define (divides? a b)
      (= (remainder b a) 0))

    (define (square a)
      (* a a))

    (define (find-divisor n test-divisor)
      (cond ((> (square test-divisor) n) n)
            ((divides? test-divisor n) test-divisor)
            (else (find-divisor n (+ test-divisor 1)))))

    (define (smallest-divisor n)
      (find-divisor n 2))

    (smallest-divisor 199)
    (smallest-divisor 1999)
    (smallest-divisor 19999)

    运行结果为:
    199
    1999
    7

    9,练习1.22

    我很郁闷,在我的机器上给出运算时间都为0(我机器太快了??),所以无法比较,程序是这样的:
    (require srfi/19)

    (define (square a)
      (* a a))

    (define (smallest-divisor n)
      (find-divisor n 2))

    (define (prime? n)
      (= n (smallest-divisor n)))

    (define (divides? a b)
      (= (remainder b a) 0))

    (define (find-divisor n test-divisor)
      (cond ((> (square test-divisor) n) n)
            ((divides? test-divisor n) test-divisor)
            (else (find-divisor n (+ test-divisor 1)))))


    (define (timed-prime-test n)
      (newline)
      (display n)
      (start-prime-test n (current-time)))

    (define (start-prime-test n start-time)
      (and (prime? n)
           (report-prime (time-difference (current-time) start-time))))

    (define (report-prime elapsed-time)
      (display " *** ")
      (display elapsed-time)
      #t)

    (define (search-for-primes from n)
      (cond ((= n 0) (newline) 'done)
            ((even? from) (search-for-primes (+ from 1) n))
            ((timed-prime-test from) (search-for- primes (+ from 2) (- n 1)))
            (else (search-for-primes (+ from 2) n))))

    (search-for-primes 1000 1)
    (search-for-primes 10000 1)
    (search-for-primes 100000 1)
    (search-for-primes 1000000 1)

     在使用系统时间以及其他的时间函数时需要引入srfi/19,它提供了许多和时间计算相关的函数,具体的可以参考这个文档:http://srfi.schemers.org/srfi-19/srfi-19.html
    另外,在DrScheme中请将语言选择为“PrettyBig”,否则通不过语法检查。下面是我得到的结果:

    Welcome to DrScheme, version 4.2.1 [3m].
    Language: Pretty Big; memory limit: 
    128 megabytes.

    1001
    1003
    1005
    1007
    1009 *** #(struct:tm:time time-duration 0 0)
    done

    10001
    10003
    10005
    10007 *** #(struct:tm:time time-duration 0 0)
    done

    100001
    100003 *** #(struct:tm:time time-duration 0 0)
    done

    1000001
    1000003 *** #(struct:tm:time time-duration 10000 0)
    done
    > 



    10,练习1.23

    基于练习1.22的优化版本,由于不能够被2整除就已经说明其肯定不能被其他偶数整除,所以一个不能被2整除的数再拿去检查其是否能被4,6,8...整除就纯属多此一举。所以:
    (define (find-divisor n test-divisor)
      (cond ((> (square test-divisor) n) n)
            ((divides? test-divisor n) test-divisor)
            (else (find-divisor n (+ test-divisor 1)))))
    中的(+ test-divisor 1) 应该修改为 ((= 2 test-divisor) 3 (+ 2 test- division))  
    其他部分和练习1.22中完全一样。由于减少了很多不必要计算,所以效率肯定提高了,提高了多少嘛,不知道,原因很简单,我的机器打印出的时间均为0(%>_<%
     

    11,练习1.24

    将原函数中的 timed-prime-test 改成 fast-prime?,运行一下就知道了。

    12,练习1.25

    对于Scheme来说,其做法应该没有什么问题,毕竟Scheme是玩数值的,所以其在求幂时不会出现其他语言的“溢出”问题(数值太大了,超出了类型所表示的范围),但我们知道算法应该不依赖于语言的,所以其算法思想不应该在会产生溢出问题的语言中推广(比如Java,c#等)。

    13,练习1.26

    道理比较简单,比如我们平时如果写下如下的代码:
    x = F ( G ( a ) ) + M ( G ( a ) )
    我们肯定会发觉这做了重复的工作,如果G是一个递归函数,这样的开销就十分可观了。
    书上给的例子是同一个道理,其不仅仅是重复地调用,而且是重复地递归调用,所以要慢很多。
    至于时间复杂度嘛,原来之所以能将其从N降低到logN,就是因为避免了这样的重复运算,现在其又回退到N了。

    14,练习1.27

    我们的代码如下:
    (define (square a) (* a a))

    (define (expmod base exp m)
      (cond ((= exp 0) 1)
            ((even? exp)
             (remainder (square (expmod base (/ exp 2) m))
                   ;      m))
            (else
             (remainder (* base (expmod base (- exp 1) m))
                   ;      m))))

    (define (try-it a n)
        (= (expmod a n n) a))

    (define (iter a n)
        (if (= a 1)
            #t
            (and (try-it a n) (iter (- a 1) n))))

    (define (fermat-test n)
      (iter (- n 1) n))

    注意到 费马小定理中式采用取随机数的方式去取下一个检测数并测试它:(try-it (+ 1 (random  (- n 1)))
    而我们的方法:
    (define (iter a n)
        (if (= a 1)
            #t
            (and (try-it a n) (iter (- a 1) n))))
    则几乎是顺序检测,算法思想不一样,故意用在这里去检测 Carmichael数,感觉多少有点作弊的嫌疑。

    注:这是一篇读书笔记,所以其中的内容仅属个人理解而不代表SICP的观点,并随着理解的深入其中的内容可能会被修改
     


     

  • 相关阅读:
    C语言开发框架、printf(day02)
    Linux C(day01)
    线程同步、信号量、system v IPC
    UDP、线程、mutex锁(day15)
    Socket编程(day14)
    共享内存、网络(day13)
    pause、jobs、setitimer(2)、system v ipc(day12)
    PIPE、SIGNAL(day11)
    环境变量、system(day10)
    进程(day09)
  • 原文地址:https://www.cnblogs.com/zhouyinhui/p/1574664.html
Copyright © 2011-2022 走看看