练习1.21
这道题几乎没有难度,除非在把书中函数写入到Edwin中时输入错误。
(smallest-divisor 199)
;Value: 199
(smallest-divisor 1999)
;Value: 1999
(smallest-divisor 19999)
;Value: 19999
练习1.22
这道题中需要判断素数的部分书中都已经列出来了,但要求是要找出多个素数,因此我们要有一个能够不断求素数的函数。在C等语言中我们可以通过一个for循环来轻易地求出来,在Scheme中我们完全可以用迭代来实现这一功能。另外因为是要的素数,因此完全不用考虑偶数了。于是我们先来写一个不断求奇数的函数。
(define (odd-after-x x)
(if (odd? x)
(+ 2 x)
(+ 1 x)))
通过将偶数加1、奇数加2,轻易地求出了x之后的奇数。
判断素数的函数我们直接列出了:
(define (prime? n)
(define (smallest-divisor n)
(find-divisor n 2))
(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 (divides? a b)
(= (remainder b a) 0))
(= n (smallest-divisor n)))
下面我们利用这两个函数来定义一个不断产生素数的函数:
(define (prime-after-prime n count)
(cond ((= count 0) (display “These are all prime numbers”))
((prime? n) (display n)
(newline)
(prime-after-prime (odd-after-x n) (- count 1)))
(else (prime-after-prime (odd-after-x n) count))))
如此一来便能够按照书中的要求算出一千、一万等的3个最小的素数了,不过还不能计算所需时间。而要求时间,我们可以用real-time-clock,这在【Scheme归纳】7中有相关介绍。下面我们就来将计算时间的功能加入进去。
(define (get-time&prime n)
(let ((start-time (real-time-clock)))
(prime-after-prime n 3) 由于题目中只要求求出3个最小的素数即可。
(- (real-time-clock) start-time)))
下面我们就来开始测试了:
(get-time&prime 1000)
1009
1013
1019
These are all prime numbers
;Value: 0
时间太短记为0了我也没办法,下面再来几组大的。
(get-time&prime 10000)
10007
10009
10037
These are all prime numbers
;Value: 0
还是0,莫非函数错了?再来一次。
(get-time&prime 100000)
100003
100019
100043
These are all prime numbers
;Value: 0
我也不知道为什么会这样了,用一百万测试的还是0,难度是i7的速度快?不过:
(get-time&prime 10000000)
10000019
10000079
10000103
These are all prime numbers
;Value: 15
继续,一亿用了47微秒,10亿用了157微秒。100亿、一千亿、一万亿用了672、1750、5156。一亿亿用了621890秒,3个最小的素数分别是10000000000000061、10000000000000069、10000000000000079。
后来在网上也看到相同的函数,而对方评论下说对方写错了。我再看看书中的内容发现果然如此,我也看错了,第36页上面说的是检查每个素数所需要的时间,应该是意味着每个素数都要有个时间吧?如是将函数改成了这样:
(define (get-time&prime n)
(let ((start-time (real-time-clock)))
(define (prime-after-prime n count)
(cond ((= count 0) (display "These are all prime numbers."))
((prime? n) (display n)
(display (- (real-time-clock) start-time))
(newline)
(prime-after-prime (odd-after-x n) (- count 1)))
(else (prime-after-prime (odd-after-x n) count)))))
(prime-after-prime n 3)))
因为觉得返回更多的素数结果会更加准确,于是我在测试的时候将最后一行代码的3改成了5。通过测试,在1000和10000中的每个素数的计算时间都是0。10万和100万也都是零。我觉得是因为这本书的历史比较久远了,当时的硬件比不上现在的了。于是我又用了一千万来测试,返回的5个时间分别是0、15、15、15、31,用一亿来测试返回的5个时间分别是15、31、47、62、78,和根号10的差距不是太遥远吧。十亿返回的5个时间分别是47、94、141、297、391。至此这道习题就算做完了,稍有不足都请读者列出。
练习1.23
首先我们按照题目要求来写出相应的next函数,然后再修改find-divisor函数。
(define (next x)
(if (= x 2)
3
(+ n 2)))
(define(find-divisor n test-divisor)
(cond ((> (square test-divisor) n) n)
((divides? test-divisor n) test-divisor)
(else (find-divisor n (next test-divisor)))))
然后重新编译之前写好的get-time&prime函数,再加以测试这道题就算完成了。
练习1.24
我们先将书中已给出的代码写入Edwin中。
(define (fermat-test n)
(define (try-it a)
(= (expmod a n n) a))
(try-it (+ 1 (random (- n 1)))))
(define (fast-prime? n times)
(cond ((= times 0) true)
((fermat-test n) (fast-prime? n (- times 1)))
(else false)))
(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))))
于是就有了一个新的prime?函数如下:
(define (prime? n)
(fast-prime? n 100))
然后载入上一题中的get-time&prime函数,如果已经在上一题中保存了起来现在就可以直接load了。然后经过一番测试后,结论很明显咯。练习1.22中的get-time&prime函数的复杂度为Θ(√n),而本题中的复杂度为Θ(logn) 。
练习1.25
这道题由Alyssa的一个另一版本的expmod来引出,这个expmod的功能和之前的一样的。但是之前版本的expmod每次都有一个remainder来讲乘幂控制在一个不大的范围内,这样通过不断的迭代,将很大的数字分解开来得以加快计算速度。而题目中这一版本的expmod则只通过了一次remainder。大家可以用2个非常大的数字来测试一番,比如几百亿之类的。
练习1.26
这本书的练习好像很多都和某个人有关,不愧是一本经典著作,通过MIT大量的修修补补。下面我们进入正题吧,Louis的问题就在于计算了2次(expmod base (/ exp 2) m),如果是用的square,则只会计算一次。更何况在这多余的一次里,又是一个漫长的迭代。Lisp最吸引我的就是它的精巧优美了,能短则短吧。
练习1.27
这道题的场景是在注释47中,博主更关心的问题是:(第三行)撞上能欺骗费马检查的值的概率有多少,居然会比什么宇宙射线造成计算机出差。后者个人感觉是永远不会发生的,前者倒是还有可能发生。希望把前者的概率算出来的童鞋将过程列出来啦!
言归正传,题目的意思就是要去验算注释47中的那几个Carmichael,那就来code吧:
(define (find-carmichael n)
(define (find-carmichael-test x n)
(cond ((= x n) #t)
((same-remainder? x n)
(find-carmichael-test (+ x 1) n))
((else #f)))
(define (same-remainder? x n)
(= (expmod x n n) x))
(find-carmichael-test 1 n))
当然了,这里要load保存好的expmod函数。然后就是测试了:
(carmichael-test 6601)
;Value: #t
其他的都一样的返回结果,不然就是函数写错了。
练习1.28
这道题主要分为三个部分:
1、非平凡平方根,并添加到expmod函数中
2、类似于fermat-test的过程
3、通过已知的素数和非素数来检验
下面我们首先来写出能够在遇到非平凡平方根的时候报错的函数,在这个函数中:当x不等于1,x不等于(n-1),并且x的平方对n取余等于1,这三个条件都为真时则可以说遇到了“1取模n的非平凡平方根”。下面是该函数:
(define (not-square-root? x n)
(and (not (= x 1))
(not (= x (- n 1)))
(= 1 (remainder (square x) n))))
然后我们要将这个函数添加到expmod中,在cond里面添加一项即可:
(define (expmod base exp m)
(cond ((= exp 0) 1)
((not-square-root? base m) 0)
((even? exp)
(remainder (square (expmod base (/ exp 2) m)) m))
(else (remainder (* base (expmod base (- exp 1) m)) m))))
第一步我们已经完成了,下面来看看第二步。在fermat-test中,已经有了一个try-it函数,但这个函数在这道题里不适用,因此我们来自己写一个产生随机数的函数。这个函数用来生成大于0并且小于n的随机数。
(define (zero-to-n-random x)
(let ((r (random x)))
(if (not (= r 0))
r
(zero-to-n-random x))))
random并不会参数负数的随机数,也不能用负数作为参数来产生随机数。下面我们来继续完成miller-rabin-prime函数。
(define (miller-rabin-prime? n)
(let ((x (ceiliing (/ n 2))))
(miller-rabin-test n x)))
(define (miller-rabin-test n x)
(cond ((= x 0) #t)
((= (expmod (zero-to-n-random n) (- n 1) n) 1)
(miller-rabin-test n (- x 1)))
(else #f)))
最后还剩下测试的工作了:
(miller-rabin-prime? 1729)
;Value: #f
(miller-rabin-prime? 2821)
;Value: #f
(miller-rabin-prime? 31)
;Value: #t