现在的位置: 首页 > 综合 > 正文

SICP学习笔记(1.2.3 ~ 1.2.6)

2012年09月16日 ⁄ 综合 ⁄ 共 5538字 ⁄ 字号 评论关闭
                                                            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的观点,并随着理解的深入其中的内容可能会被修改
 

 

抱歉!评论已关闭.