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”,否则通不过语法检查。下面是我得到的结果:
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的观点,并随着理解的深入其中的内容可能会被修改