1. 程式人生 > >《SICP》習題第1章

《SICP》習題第1章

本人做的SICP習題第1章,如有錯誤請指正,用的直譯器是Racket

練習1.1

計算程式碼如下

;; Exercise 1.1
#lang racket
10

(+ 5 3 4)

(- 9 1)

(/ 6 2)

(+ (* 2 4) (- 4 6))

(define a 3)

(define b (+ a 1))

(+ a b (* a b))

(= a b)

(if (and (> b a) (< b (* a b)))
    b
    a)

(cond ((= a 4) 6)
      ((= b 4) (+ 6 7 a))
      (else 25))

(+ 2 (if (> b a) b a))

(* (cond ((> a b) a)
         ((< a b) b)
         (else -1))
   (+ a 1))

10

12

8

3

6

a

b

19

#f

4

16

6

16

 

練習1.2

;; Exercise 1.2
#lang racket

(/ (+ 5 4 (- 2 (- 3 (+ 6 (/ 4 5)))))
   (* 3 (- 6 2) (- 2 7))

答案為-37/150

 

練習1.3

;; Exercise 1.3
;; 返回三個數中較大兩個數的平方和
#lang racket

;; 返回兩個輸入的較小數
(define (min a b)
  (if (< a b)
      a
      b))

;; 返回三個數中最小的一個
(define (min-three a b c)
  (min (min a b)
       (min b c)))

;; 求平方
(define (square x)
  (* x x))

;; 返回三個數中較大兩個數的平方和
;; 先計算三個數的平方和,再減去最小數的平方
(define (two-larger-square-sum a b c)
  (- (+ (square a) (square b) (square c))
     (square (min-three a b c))))

 

練習1.4

講道理嘛,這個看函式名兒都能看出來

不過這裡可以看出,操作符也可以作為一個combination的返回值

;; Exercise 1.4
#lang racket

(define (a-plus-abs-b a b)
  ((if (> b 0) + -) a b))

a + abs(b)

 

練習1.5

這題我卡了非常久,第一次做的時候沒有仔細想就隨便過去了,做到後面1.20的時候覺得不對,又回來看1.5和1.6兩題,然後畫了一天理解什麼是應用序和正則序,具體的思路整理在另一篇部落格裡刷SICP遇到的問題——深入學習理解正則序和應用序

看一下題目裡的這段程式碼

;; Exercise 1.5
#lang racket

;; 定義一個死迴圈遞迴
(define (p) (p))

;; 檢測直譯器是應用序還是正則序
(define (test x y)
  (if (= x 0)
      0
      y))

(test 0 (p))

(define (p) (p))定義了一個死迴圈函式,這是一個最簡單的遞迴,所以一旦求解(p)這個表示式,就是進入這個遞迴死迴圈

如果是應用序,在執行(test 0 (p))的時候就會去求解p,我試了MIT-Scheme和Racket兩個直譯器,都會卡住,顯然都是應用序

如果是正則序,會正常返回0,在執行(test 0 (p))後就替換為(if (= 0 0) 0 (p)),條件表示式滿足,(p)就被忽略掉了,所以會正常返回0,我試了lazyracket這個惰性求值的直譯器(實在找不到正則序的),可以返回0。切換到lazyracket很簡單,開頭那裡定義成#lang lazy就行

 

練習1.6

這段程式碼

;; Exercise 1.6
#lang racket

;; 定義新if
(define (new-if predicate then-clause else-clause)
  (cond (predicate then-clause)
        (else else-clause)))

;; 求平方
(define (square x)
  (* x x))

;; 檢測猜測值精度
(define (good-enough? guess x)
  (< (abs (- (square guess) x)) 0.001))

;; 牛頓法改進猜測值
(define (improve guess x)
  (average guess (/ x guess)))

;; 求平均值
(define (average x y)
  (/ (+ x y) 2))

;; 不斷改進猜測值直到精度滿足需求
(define (sqrt-iter guess x)
  (begin
    (new-if (good-enough? guess x)
        guess
        (sqrt-iter (improve guess x) x))))

;; 牛頓法求平方根
(define (sqrt x)
  (sqrt-iter 1.0 x))

對於應用序,必然會卡死啊,在執行一個new-if的時候求解每個引數的值,這樣就陷入了sqrt-iter的無限遞迴

對於正則序,展開一個看看,(sqrt-iter 1.0 9)展開為

(cond ((good-enough? 1.0 9) guess)
          (else (sqrt-iter (improve guess x)
                                  x))))

後面的不展開了,如果cond也是特殊處理過的,所以在某次good-enough?返回為真的情況下,不再繼續往下展開sqrt-iter,程式可以正確執行,所以我猜測在正則序下這個程式碼是可以執行的

驗證一下,用lazyracket

妥妥滴

 

練習1.7

對於很小的數,精度不足,這個非常好理解

對於很大的數,會卡死,因為浮點數的位數是有限的,無法達到0.001這麼小的精度差,比如2130895720398745908273049857902374590723904570237409572394750937252394892817059712093847098129034709823904812093875091620934213089572039874590827304985790237459072390457023740957239475093725這個數就會在原來的sqrt程式那裡卡死

我知道怎麼退出了,在執行視窗連按兩個C-c

改進good-enough?,監視猜測值guess,如果improve出的新guess和原guess差小於某個值就停止迭代,程式碼如下

;; Exercise 1.7
;; 改進的牛頓迭代法
#lang racket

;; 平方
(define (square x)
  (* x x))

;; 檢測猜測值精度,當猜測值和上一次猜測值相比改進小於0.1%時停止迭代
(define (good-enough? old-guess guess)
  (< (abs (- old-guess guess)) (* guess 0.001)))

;; 改進猜測值
(define (improve guess x)
  (average guess (/ x guess)))

;; 平均值
(define (average x y)
  (/ (+ x y) 2))

;; 牛頓迭代法
(define (sqrt-iter old-guess guess x)
  (begin
    (if (good-enough? old-guess guess)
        guess
        (sqrt-iter guess (improve guess x) x))))

;; 改進的求平方根
(define (better-sqrt x)
  (sqrt-iter 0.0 1.0 x))

 

練習1.8

程式碼如下

;; Exercise 1.8
#lang racket

;; 立方
(define (cube x)
  (* x x x))

;; 猜測值精度檢測
(define (good-enough? guess x)
  (< (abs (- (cube guess) x)) 0.001))

;; 迭代改進猜測值
(define (improve guess x)
  (/ (+ (/ x (* guess guess)) (* 2 guess)) 3))

;; 牛頓迭代法
(define (cube-root-iter guess x)
  (begin
    (if (good-enough? guess x)
        guess
        (cube-root-iter (improve guess x) x))))

;; 求立方根
(define (cube-root x)
  (cube-root-iter 1.0 x))

(cube-root 729)

 

練習1.9

第一個程式

;; Exercise1.9
;; 遞迴加法
#lang racket

;; 減一
(define (dec a)
  (- a 1))

;; 加一
(define (inc a)
  (+ a 1))

;; 遞迴加法
(define (add a b)
  (if (= a 0)
      b
      (inc (add (dec a) b))))

展開:

(+ 4 5)

(inc (+ 3 5))

(inc (inc (+ 2 5)))

(inc (inc (inc (1 5))))

(inc (inc (inc (inc 5))))

遞迴

第二個程式

;; Exercise1.9
;; 迴圈加法
#lang racket

;; 減一
(define (dec a)
  (- a 1))

;; 加一
(define (inc a)
  (+ a 1))

;; 迴圈加法
(define (add a b)
  (if (= a 0)
      b
      (add (dec a) (inc b))))

展開:

(+ 4 5)

(+ 3 6)

(+ 2 7)

(+ 1 8)

(+ 0 9)

迴圈

 

練習1.10

程式碼如下,用於驗證計算結果

;; Exercise 1.10
;; 阿克曼函式
#lang racket

;; 阿克曼函式
(define (A x y)
  (cond ((= y 0) 0)
        ((= x 0) (* 2 y))
        ((= y 1) 2)
        (else (A (- x 1)
                 (A x (- y 1))))))

(A 1 10) :

(A 1 10) = (A 0 (A 1 9)) = (* 2 (A 1 9))

(A 1 9) = (A 0 (A 1 8)) = (* 2 (A 1 8))

以此類推 (A 1 10) = (* 512 (A 1 1)) = 1024

因此可以看出來(A 1 n)就是2^n

(A 2 4):

(A 2 4) = (A 1 (A 2 3))

(A 2 3) = (A 1 (A 2 2))

(A 2 2) = (A 1 (A 2 1))

所以(A 2 4) = 2^(2^(2^2))) = 65536

可以看出來(A 2 n)就是n個2這樣 2^(2^(2^……)))

(A 3 3):

(A 3 3) = (A 2 (A 3 2)))

(A 3 2) = (A 2 (A 3 1)))  = (A 2 2) = 4

所以(A 3 3) = (A 2 4) = 65536

(define (f n) (A 0 n)):

(A 0 n) = (* 2 n) = 2n

(define (g n) (A 1 n)):

(A 1 n) = (A 0 (A 1 (n -1))) = (* 2 (A 1 (n - 1))) = …… = 2^(n-1) * (A 1 1) = 2^n

(define (h n) (A 2 n)):

(A 2 n) = (A 1 (A 2 (n - 1))) = 2^(A 2 (n - 1)) = …… = 2^(2^(2^……(A 2 1)))  = 2^(2^(2^……)))(共n個2)

(define (k n) (* 5 n n)):

5n^2啦

 

練習1.11

遞迴的程式碼

;; Exercise 1.11
;; 遞迴求f函式值
#lang racket

;; 遞迴求f
(define (f n)
  (cond ((< n 3) n)
        (else (+ (f (- n 1)) (* 2 (f (- n 2))) (* 3 (f (- n 3)))))))

迭代的程式碼

;; Exercise 1.11
;; 迭代求f函式值
#lang racket

;; 迭代求f
(define (f-iterative f1 f2 f3 n)
  (cond ((< n 3) n)
        ((= n 3) f1)
        (else (f-iterative (+ f1 (* 2 f2) (* 3 f3)) f1 f2 (- n 1)))))

;; 求f
(define (f n)
  (f-iterative 4 2 1 n))

 

練習1.12

row和col從1開始,嚴格來說這個函式是不嚴謹的,沒有考慮輸入不合法的情況

;; Exercise 1.12
;; 求pascal三角中的數值
#lang racket

;; 求第row行第col列的pascal三角數值
(define (pascal row col)
  (cond ((or (= col 1) (= col row)) 1)
        (else (+ (pascal (- row 1) col) (pascal (- row 1) (- col 1))))))

 

練習1.13

先用數學歸納法證明Fib(n) = \frac{\Phi ^{n} - \Psi ^{n}}{\sqrt{5}},在我遙遠模糊的記憶中數學歸納法的格式差不多應該是這樣,如果不符合規範,反正是我初中老師就這麼教的

1° 當n = 0,代入數值計算可得Fib(0) = \frac{\Phi ^{0} - \Psi ^{0}}{\sqrt{5}}

    當n = 1,代入數值計算可得Fib(1) = \frac{\Phi ^{1} - \Psi ^{1}}{\sqrt{5}}

2° 假設存在

          Fib(n-1) = \frac{\Phi ^{n-1} - \Psi ^{n-1}}{\sqrt{5}}以及Fib(n-2) = \frac{\Phi ^{n-2} - \Psi ^{n-2}}{\sqrt{5}}

    則有

          Fib(n) = Fib(n-1) + Fib(n-2) = \frac{\Phi ^{n-1} - \Psi ^{n-1}}{\sqrt{5}} + \frac{\Phi ^{n-2} - \Psi ^{n-2}}{\sqrt{5}}

                       = \frac{\Phi ^{n-2}(\Phi+1) - \Psi ^{n-2}(\Psi+1)}{\sqrt{5}}

                       = \frac{\Phi ^{n}- \Psi ^{n}}{\sqrt{5}}

3° 由1°、2°,可得Fib(n) = \frac{\Phi ^{n} - \Psi ^{n}}{\sqrt{5}}

 

公式真難寫,趕快儲存一下

 

現在證明\frac{\Phi ^{n}}{\sqrt{5}}最接近的整數是Fib(n)

計算可得\Psi \approx -0.618,因此\left | \frac{\Psi^{n}}{\sqrt{5}} \right | < 0.5

由二項式的展開式可得,\frac{\Phi ^{n}}{\sqrt{5}}\frac{\Psi ^{n}}{\sqrt{5}}必然不是整數

一個非整數減去一個絕對值小於0.5的非整數,所得到的整數必然是最接近這個非整數的整數嘛(取整有兩個方向,向上取整或向下取整,現在朝一個取整方向的距離小於0.5,那到另一個取整方向的距離必然大於0.5了)

 

練習1.14

我非常無聊地真的畫了出來,第一次畫完發現,我把硬幣的大小弄反了,題目程式碼是從大到小,我變成從小到大,導致多了非常多步驟,不管,錯誤的圖我也要貼出來

底下這張是正確的,我並沒有檢查,應該也沒有無聊的人會去檢查,一共標紅的4種情況是可以的

space是\Theta (n),step是\Theta (\alpha ^{n}}), 1<\alpha <2

 

練習1.15

a. \log_3{(12.15 / 0.1)} \approx 4.37,所以呼叫了4次p函式

b. space和step都是\Theta (\log{n})

 

練習1.16

;; Exercise 1.16
#lang racket

;; 迭代求冪
(define (exp-iter a b n)
  (if (= n 0) a
      (if (even n) (exp-iter a (square b) (/ n 2))
          (exp-iter (* a b) b (- n 1)))))

;; 快速求冪
(define (fast-exp b n)
  (exp-iter 1 b n))

;; 判斷是否為偶數
(define (even n)
  (= (remainder n 2) 0))

;; 平方
(define (square x)
  (* x x))

 

練習1.17

;; Exercise 1.17
;; 快速乘法
#lang racket

;; 倍增
(define (double x)
  (+ x x))

;; 減半
(define (halve x)
  (/ x 2))

;; 檢測是否為偶數
(define (even x)
  (= (remainder x 2) 0))

;; 快速乘法
(define (fast-mult a b)
  (cond ((= b 0) 0)
        ;; 處理b為負的情況
        ((< b 0) (- 0 (fast-mult a (- 0 b))))
        ((even b) (double (fast-mult a (halve b))))
        (else (+ a (fast-mult a (- b 1))))))

 

練習1.18

遞迴改迴圈很簡單,不要對遞迴呼叫的返回值做額外計算就是迭代了

;; Exercise 1.18
;; 迭代快速乘法
#lang racket

;; 倍增
(define (double x)
  (+ x x))

;; 減半
(define (halve x)
  (/ x 2))

;; 檢測是否為偶數
(define (even x)
  (= (remainder x 2) 0))

;; 快速乘法
(define (fast-mult-iterative a b)
  (cond ((= b 0) 0)
        ;; 處理b為負的情況
        ((< b 0) (- 0 (fast-mult-iterative a (- 0 b))))
        ;; 改遞迴為迭代
        ((even b) (fast-mult-iterative (double a) (halve b)))
        (else (+ a (fast-mult-iterative a (- b 1))))))

 

練習1.19

首先證明,變換T_{pq}: a\leftarrow bq+aq+ap, b\leftarrow bp+aq

兩次變換相當於一次變換T_{p'q'}

a_1\leftarrow b_0q+a_0q+a_0p, b_1\leftarrow b_0p+a_0q

a_2\leftarrow (b_0p+a_0q)q+(b_0q+a_0q+a_0p)(q+p)

a_2\leftarrow (2pq+q^2)b_0+(2q^2+p^2+2pq)a_0

b_2\leftarrow (b_0p+a_0q)p+(b_0q+a_0q+a_0p)q

b_2\leftarrow (p^2+q^2)b_0+(2pq+q^2)a_0

則一次變換T_{p'q'}相當於兩次變換T_{pq},其中

p' = p^2+q^2, q' = 2pq+q^2

對於裴波那契數,則有

Fib(n-1) = Fib(n-2) + Fib(n-3), Fib(n) = Fib(n-1) + Fib(n-2)

Fib(n) = Fib(n-1) + Fib(n-2), Fib(n+1) = Fib(n) + Fib(n-1)

a_{n-1} \rightarrow Fib(n-1), b_{n-1} \rightarrow Fib(n-2)

 a_{n} = a_{n-1} + b_{n-1}, b_{n} = a_{n-1}

相當於變換T_{01},兩次變換T_{01}可以用一次變換T_{11}代替,即

a_{n} = 2a_{n-2} + b_{n-2}, b_{n} = a_{n-1}

程式碼如下

;; Exercise 1.19
;; 快速求解裴波那契數
#lang racket

;; 求解裴波那契數
(define (fast-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
                   (+ (square p) (square q))
                   (+ (* 2 p q) (square q))
                   (/ count 2)))
        (else (fib-iter (+ (* b q) (* a q) (* a p))
                        (+ (* b p) (* a q))
                        p
                        q
                        (- count 1)))))

;; 平方
(define (square x)
  (* x x))

;; 判斷是否為偶數
(define (even? x)
  (= (remainder x 2) 0))

 

練習1.20

正則序會比應用序多呼叫remainder灰常多次,因為a、b在正則序裡都會展開為remainder求值

用下面這段程式在racket和MIT-Scheme兩個直譯器上測試

;; Exercise 1.20
;; 計算歐幾里得演算法呼叫remainder函式次數
#lang racket

;; 重新定義remainder函式,每次呼叫都打出一個yes
(define (remainder-count a b)
  (display "yes\n")
  (remainder a b))

;; 歐幾里得演算法
(define (gcd a b)
  (if (= b 0)
      a
      (gcd b (remainder-count a b))))

(gcd 206 40)

racket是應用序,一共輸出4個yes,就是呼叫4次remainder

正則序展開,我只展開了一點,完全展開的話,畫面太美……

(gcd 206 40)

(if (= b 0) a ((if (= (remainder a b) 0) b (gcd (remainder a b) (remainder b (remainder a b))))))))

……

總之,非常多remainder呼叫

這個我用lazyracket也試了一下,照樣返回4個yes,所以惰性求值和正則序還是有區別的,不會簡單粗暴地一擼到底

現在來計算次數,我是不會展開的,用了另一種方法來做

呼叫的過程是(gcd 206 40) → (gcd 40 6) → (gcd 6 4) → (gcd 4 2) ,逆推回去

(gcd 2 0)執行了:

(if (= 0 0)

    2))

這裡呼叫1個0,1個2,0通過(remainder 4 2)得到,2通過(remainder 6 4)得到,4通過(remainder 40 6)得到,6通過(remainder 206 40)得到

所以(gcd 2 0)裡面,0通過6次remainder得到,2通過4次remainder,4通過2次remainder,6通過1次remainder,一共呼叫了13次

對於其他的gcd過程,尾遞迴的(gcd b (remainder a b))中a和b只是傳遞到了下一層,最終傳遞到(gcd 2 0)才開始規約,所以不需要考慮,if條件的a分支在條件判斷後就被跳過,真正呼叫了remainder的只有條件判斷裡的b

對於(gcd 4 2),判斷2是否為0,通過4次remainder得到

對於(gcd 6 4),判斷4是否為0,通過1次remainder得到

對於(gcd 206 40),判斷40是否為0,沒有呼叫remainder

所以加起來是13 + 4 + 1 = 18

網上有人做了完全展開,結果也是18,見這篇博文SICP_exercise_1.20

 

練習1.21

求解程式碼

;; 求輸入的最小因子
#lang racket
(provide (all-defined-out))
 
;; 平方
(define (square x)
  (* x x))

;; 求最小因子
(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))

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

199和1999都是質數,19999最小因子是7

 

練習1.22

我用的是racket,沒有runtime,用了current-inexact-milliseconds代替

引用了練習1.21的最小因數函式

先寫一個判斷是否是質數的程式碼

;; 通過求解最小因數,判斷是否是質數
#lang racket
(provide (all-defined-out))
(require "smallest-divisor.rkt")

;; 最小因數等於本身,證明為質數
(define (prime-test-by-smallest-divisor x)
  (and (> x 1) (= (smallest-divisor x) x)))

然後寫一個搜尋質數,現在計算機太快,題目裡那麼小的數一會就算完了,所以加大了很多倍

;; Exercise 1.22
;; 搜尋給定範圍的質數,並列印時間
;; 判斷質數使用最小因子法
#lang racket
(require "prime-test-by-smallest-divisor.rkt")

;; 平方
(define (square x)
  (* x x))

;; 搜尋指定數量和範圍的質數並列印
(define (start-prime-test n start-time number)
  (cond ((= number 0) (report-prime (- (current-inexact-milliseconds) start-time)))
        ((prime-test-by-smallest-divisor n)
         (display n)
         (display " ")
         (start-prime-test (+ n 1) start-time (- number 1)))
        (else (start-prime-test (+ n 1) start-time number))))

;; 列印程式耗費時間
(define (report-prime elapsed-time)
  (display "*** ")
  (display elapsed-time)
  (newline))

;; 開始質數搜尋,並記錄當前時間
(define (searchForPrimes startNumber number)
  (start-prime-test startNumber (current-inexact-milliseconds) number))      
;; 求冪的模
(define (expmod base n m)
  (cond ((= n 0) 1)
        ((odd? n) (remainder (* base (expmod base (- n 1) m)) m))  
        (else (remainder (square (expmod base (/ n 2) m)) m))))

(searchForPrimes 10000000000 3)
(searchForPrimes 100000000000 3)
(searchForPrimes 1000000000000 3)
(searchForPrimes 10000000000000 3)
(searchForPrimes 100000000000000 3)
n 時間 比例
10000000000 9.894  
100000000000 28.444 2.874874
1000000000000 74.334 2.613346
10000000000000 302.888 4.07469
100000000000000 989.38 3.266488

好像並不是十分地嚴格啊,當n增加比例會相對於近似理論值

 

練習1.23

改進的最小因子求解程式碼如下

;; Exercise 1.23
;; 改進的求最小因子函式
;; 當不能被2整除,跳過所有偶數因子的嘗試
#lang racket
(provide (all-defined-out))

;; 迭代搜尋最小因子
(define (fast-divisor-iter n divisor)
  (cond ((> (* divisor divisor) n) n)
        ((divides? n divisor) divisor)
        (else (fast-divisor-iter n (next divisor)))))

;; 改進的求解最小因子函式
(define (fast-smallest-divisor n)
  (fast-divisor-iter n 2))

;; 判斷是否可以除盡
(define (divides? a b)
  (= (remainder a b) 0))

(define (next divisor)
  (if (= divisor 2)
      3
      (+ divisor 2)))

改進後的質數測試

;; Exercise 1.23
;; 改進版通過求解最小因數,判斷是否是質數
#lang racket
(provide (all-defined-out))
(require "fast-smallest-divisor.rkt")

;; 最小因數等於本身,證明為質數
(define (fast-prime-test-by-smallest-divisor x)
  (= (fast-smallest-divisor x) x))

改進後的質數搜尋

;; Exercise 1.22
;; 搜尋給定範圍的質數,並列印時間
;; 判斷質數使用最小因子法
#lang racket
(require "fast-prime-test-by-smallest-divisor.rkt")

;; 平方
(define (square x)
  (* x x))

;; 搜尋指定數量和範圍的質數並列印
(define (start-prime-test n start-time number)
  (cond ((= number 0) (report-prime (- (current-inexact-milliseconds) start-time)))
        ((fast-prime-test-by-smallest-divisor n)
         (display n)
         (display " ")
         (start-prime-test (+ n 1) start-time (- number 1)))
        (else (start-prime-test (+ n 1) start-time number))))

;; 列印程式耗費時間
(define (report-prime elapsed-time)
  (display "*** ")
  (display elapsed-time)
  (newline))

;; 開始質數搜尋,並記錄當前時間
(define (searchForPrimes startNumber number)
  (start-prime-test startNumber (current-inexact-milliseconds) number))      
;; 求冪的模
(define (expmod base n m)
  (cond ((= n 0) 1)
        ((odd? n) (remainder (* base (expmod base (- n 1) m)) m))  
        (else (remainder (square (expmod base (/ n 2) m)) m))))

(searchForPrimes 10000000000 3)
(searchForPrimes 100000000000 3)
(searchForPrimes 1000000000000 3)
(searchForPrimes 10000000000000 3)
(searchForPrimes 100000000000000 3)

替換之後,時間如下

n 時間 時間/改進前時間
10000000000 4.064 41.08%
100000000000 16.261 57.17%
1000000000000 43.798 58.92%
10000000000000 186.143 61.46%
100000000000000 603.598 61.01%

時間大約縮短了一小半,貌似越大的數縮小的越少

 

練習1.24

先寫好費馬小定理對應的質數檢驗函式

;; Exercise 1.24
;; 通過費馬小定理檢驗是否為質數
#lang racket
(provide (all-defined-out))

;; 求冪的取模
(define (expmod base n m)
  (cond ((= n 0) 1)
        ((odd? n) (remainder (* base (expmod base (- n 1) m)) m))  
        (else (remainder (square (expmod base (/ n 2) m)) m))))

;; 平方
(define (square x)
  (* x x))

;; 判斷是否為奇數
(define (odd? n)
  (= (remainder n 2) 1))

;; 一次費馬小定理的質數檢驗
(define (fermat-test n)
  (define (try-it a)
    (= (expmod a n n) a))
  (try-it (+ 1 (random (- n 1)))))

;; 費馬小定理檢驗質數
(define (prime-test-by-fermat-iter n times)
  (cond ((= times 0) true)
        ;; 若一次檢驗通過,次數減一,再次檢驗
        ((fermat-test n) (prime-test-by-fermat-iter n (- times 1)))
        (else false)))

;; 費馬小定理檢驗是否為質數,n表示檢驗次數
(define (prime-test-by-fermat n)
  (prime-test-by-fermat-iter n 10))

然後編寫改進後的質數搜尋函式,因為時間實在太小,把數增加到很大,尋找質數的個數也設為了3000

;; Exercise 1.24
;; 通過費馬小定理檢驗質數法,搜尋給定範圍內的質數
#lang racket
(require "prime-test-by-fermat.rkt")

;; 平方
(define (square x)
  (* x x))

;; 搜尋指定數量和範圍的質數並列印
(define (start-prime-test n start-time number)
  (cond ((= number 0) (report-prime (- (current-inexact-milliseconds) start-time)))
        ((prime-test-by-fermat n)
         (start-prime-test (+ n 1) start-time (- number 1)))
        (else (start-prime-test (+ n 1) start-time number))))

;; 列印程式耗費時間
(define (report-prime elapsed-time)
  (display "*** ")
  (display elapsed-time)
  (newline))

;; 開始質數搜尋,並記錄當前時間
(define (searchForPrimes startNumber number)
  (start-prime-test startNumber (current-inexact-milliseconds) number))

(searchForPrimes 1000000 3000)
(searchForPrimes 100000000 3000)

運行了三次,結果如下,理論上呢大輸入的執行時間應該是小輸入的兩倍,實際是比兩倍少一些

這個原因大家應該都很清楚,程式的用時不是嚴格等於執行的步數,還有很多其他因素,這裡不多說

 

練習1.25

這段程式碼理論上是可以執行的,實踐中,對於較小的數下面程式可以執行

但是輸入增加後,冪增加到很大,最後的計算結果是一個灰常大的整數,scheme處理灰常大的整數會很卡

而之前的函式可以很快返回結果

因為之前的函式每迭代一次求冪,就取模一次,所以冪(的餘數)一直限制在一個很小的範圍

 

練習1.26

因為expmod在每次迭代過程中都計算了兩次

討論個理想情況,假設exp = N = 2^n,在計算expmod的時候的呼叫如下:

(expmod, 2^n)呼叫了2次(expmod, 2^{n-1})

(expmod, 2^{n-1})呼叫了2次(expmod, 2^{n-2})

……

(expmod, 2^{1})呼叫了2次(expmod, 2^{0})

總呼叫次數為:1+2^1+2^2+......+2^{n} = 2^{n+1} - 1 = 2N - 1

對於不完全等於2次冪的N來說,也有類似的結果,證明這個程式是\Theta (n)

 

練習1.27

做了十次費馬小定理檢驗,這6個數字都通過了

這種數叫啥正合成數,也叫偽素數,他們是可以通過費馬小定理的檢驗的

 

練習1.28

修改一下square函式,在平方後做一下檢測

;; Exercise 1.28
;; 通過Miller-Rabin測試檢驗是否為質數
#lang racket

;; 求冪的取模,若檢測到某次迭代冪模為1,返回0,其餘情況返回冪的取模結果
(define (expmod base n m)
  (cond ((= n 0) 1)
        ((odd? n) (remainder (* base (expmod base (- n 1) m)) m))  
        (else (remainder (square-and-check (expmod base (/ n 2) m) m) m))))

;; 平方,檢測平方結果對m的模,為1則返回0,其餘情況返回平方結果
(define (square-and-check x m)
  (define res (* x x))
  (cond ((or (= x 1) (= x (- m 1))) res)
        ((= (remainder res m) 1) 0)
        (else res)))

;; 判斷是否為奇數
(define (odd? n)
  (= (remainder n 2) 1))

;; 一次質數檢驗
(define (fermat-test n)
  (define (try-it a)
    (= (expmod a n n) a))
  (try-it (+ 1 (random (- n 1)))))

;; 質數檢驗
(define (prime-test-by-miller-rabin-iter n times)
  (cond ((= times 0) true)
        ;; 若一次檢驗通過,次數減一,再次檢驗
        ((fermat-test n) (prime-test-by-miller-rabin-iter n (- times 1)))
        (else false)))

;; 檢驗是否為質數,n表示檢驗次數
(define (prime-test-by-miller-rabin n)
  (prime-test-by-miller-rabin-iter n 10))

 

練習1.29

不要以a作為自變數,轉變思路,用k做自變數寫起來更加方便

;; Exercise 1.29
;; Simpson's Rule計算integral
#lang racket
(require "cube.rkt")
(require "sum.rkt")

;; Simpson's Rule計算integral
(define (integral-by-simpson f a b n)
  ;; 定義h
  (define h (/ (- b a) n))
  ;; 定義h/3
  (define h-divide-3 (/ h 3))
  ;; 以k作為自變數,比用a方便
  (define (next k)
    (+ k 1))
  ;; k的每項計算
  (define (term k)
    (define yk (* h-divide-3 (f (+ a (* k h))))) 
    (cond ((or (= k 0) (= k n)) yk)
          ((even? k) (* 2 yk))
          (else (* 4 yk))))
  ;; 呼叫sigma函式
  (sum term 0 next n))

;; 判斷是否為偶數
(define (even? x)
  (= (remainder x 2) 0))

(integral-by-simpson cube 0 1 100)
(integral-by-simpson cube 0 1 1000)

cube就是求立方的程式碼很簡單,如下

;; 立方計算
#lang racket
(provide (all-defined-out))

(define (cube x)
  (* x x x))

sum是求sigma的

;; sigma計算
#lang racket
(provide (all-defined-out))

(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

n是100或者1000出來的結果都是1/4

 

練習1.30

寫出迭代的程式,技巧就是遞迴呼叫的時候,外面不要再套其他操作

;; Exercise 1.30
;; 迭代版的sigma
#lang racket

(define (sum term a next b)
  (define (sum-iter a res)
    (if (> a b)
        res
        (sum-iter (next a) (+ res (term a)))))
  (sum-iter a 0))

 

練習1.31

a.

遞迴的,答案是3.141592810665997

;; Exercise 1.31
;; 遞迴的product
#lang racket

;; 遞迴的product
(define (product term a next b)
  (if (> a b)
      1
      (* (product term (next a) next b)
         (term a))))

;; 判斷是否為偶數
(define (even? x)
  (= (remainder x 2) 0))

;; 計算pi
(define (pi-product n)
  (define (next k)
    (+ k 1))
  (define (term k)
    (/ (if (even? k)
           (+ k 2.0)
           (+ k 1.0))
       (if (even? k)
           (+ k 1.0)
           (+ k 2.0))))
  (* (product term 1 next n)
     4))

(pi-product 10000000)

b.

迭代的,答案略有不同,是3.1415928106682323,因為乘的先後順序不一樣了

;; Exercise 1.31
;; 迭代的product
#lang racket

;; 迭代的product
(define (product term a next b)
  (define (product-iter a res)
    (if (> a b)
        res
        (product-iter (next a) (* res (term a)))))
  (product-iter a 1))

;; 判斷是否為偶數
(define (even? x)
  (= (remainder x 2) 0))

;; 計算pi
(define (pi-product n)
  (define (next k)
    (+ k 1))
  (define (term k)
    (/ (if (even? k)
           (+ k 2.0)
           (+ k 1.0))
       (if (even? k)
           (+ k 1.0)
           (+ k 2.0))))
  (* (product term 1 next n)
     4))

(pi-product 10000000)

 

練習1.32

a.

遞迴版本

;; Exercise 1.32
;; 遞迴的accumulate
#lang racket

;; 遞迴accumulate
(define (accumulate combiner null-value term a next b)
  (if (> a b)
      null-value
      (combiner (accumulate combiner null-value term (next a) next b)
                (term a))))

b.

迭代版本

;; Exercise 1.32
;; 迭代的accumulate
#lang racket

;; 迭代accumulate
(define (accumulate combiner null-value term a next b)
  (define (accumulate-iter a res)
    (if (> a b)
        res
        (accumulate-iter (next a) (combiner res (term a)))))
  (accumulate-iter a null-value))

 

練習1.33

加條件判斷的accumulate,簡單

;; Exercise 1.33
;; 帶filter的accumulate,迭代計算
#lang racket
(provide (all-defined-out))

;; 迭代filtered-accumulate
(define (filtered-accumulate combiner null-value term a next b filter?)
  (define (accumulate-iter a res)
    (cond ((> a b) res)
          ((filter? a) (accumulate-iter (next a) (combiner res (term a))))
          (else (accumulate-iter (next a) res))))
  (accumulate-iter a null-value))

a.

加一個素數判斷,用上之前的程式碼

;; Exercise 1.33
;; 素數累加
#lang racket
(require (file "../Testing for Primality/prime-test-by-smallest-divisor.rkt"))
(require "filtered-accumulate.rkt")

(define (prime-accumulate a b)
  (define (next x)
    (+ x 1))
  (filtered-accumulate + 0 identity a next b prime-test-by-smallest-divisor))

(prime-accumulate 1 10)

b.

也很簡單,呼叫之前寫的歐幾里得演算法

;; Exercise 1.33
;; 所有與n互質且小於n的正整數累乘
#lang racket
(require "filtered-accumulate.rkt")

;; 歐幾里得演算法
(define (gcd a b)
  (if (= b 0)
      a
      (gcd b (remainder a b))))

;; n以內,與n互質的正整數乘積
(define (prime-to-n-accumulate n)
  (define (filter? x)
    (= (gcd x n) 1))
  (define (next x)
    (+ x 1))
  (filtered-accumulate * 1 identity 1 next n filter?))

 

練習1.34

這段程式碼會報錯,實際上最終呼叫了(2 2),由於2不是一個procedure,報錯

 

練習1.35

黃金分割滿足\Phi ^2 = \Phi +1,兩邊同除得\Phi = \Phi +\frac{1}{\Phi}

;; Exercise 1.35
;; 利用fixed-point計算黃金分割
#lang racket
(require "fixed-point.rkt")

;; 計算近似黃金分割
(define (golden-ratio)
  (fixed-point (lambda(x) (+ 1.0 (/ 1 x))) 1))

(golden-ratio)

 

練習1.36

;; Exercise 1.36
;; 利用fixed-point計算x^x = 1000,並列印中間結果
#lang racket

;; 設定精度
(define tolerance 0.00001)

;; fixed-point計算
(define (step-display-fixed-point f first-guess)
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (define (try guess)
    (let ((next (f guess)))
      (display next)
      (newline)
      (if (close-enough? guess next)
          next
          (try next))))
  (try first-guess))

(step-display-fixed-point (lambda (x) (/ (log 1000) (log x))) 4)

打印出來的中間結果如下

 

練習1.37

a.

遞迴版本的如下

;; Exercise 1.37
;; 遞迴,近似計算無限連續小數
#lang racket

;; 計算無限連續小數
(define (cont-frac-recursive d n k)
  (define (cont-frac-recursive-iter i)
    (if (> i k)
        0
        (/ (n i)
           (+ (d i)
              (cont-frac-recursive-iter (+ i 1))))))
  (cont-frac-recursive-iter 1))

(cont-frac-recursive (lambda (i) 1.0)
                     (lambda (i) 1.0)
                     11)

測下來k=11的時候才開始得到0.61805,約等於0.6181,4位小數精度

 

b.

迭代版本

;; Exercise 1.37
;; 迭代,近似計算無限連續小數
#lang racket

;; 計算無限連續小數
(define (cont-frac-iterative d n k)
  (define (cont-frac-iterative-iter i res)
    (if (= i 0)
        res
        (cont-frac-iterative-iter (- i 1)
                                  (/ (n i)
                                     (+ (d i) res)))))
  (cont-frac-iterative-iter k 0))

(cont-frac-iterative (lambda (i) 1.0)
                     (lambda (i) 1.0)
                     11)

 

練習1.38

;; Exercise 1.38
;; 計算e-2
#lang racket
(require "cont-frac-iterative.rkt")

;; 計算d,規律為121,141,161
(define (d x)
  (if (< (remainder x 3) 2)
      1.0
      (* 2.0 (/ (+ x 1) 3))))

;; 計算e-2
(cont-frac-iterative d
                     (lambda(x) 1.0)
                     10)

 

練習1.39

;; Exercise 1.39
;; 近似計算tan
#lang racket
(require "cont-frac-iterative.rkt")

;; 計算近似tan
(define (tan-cf x k)
  (/ (cont-frac-iterative (lambda(y) (- (* y 2) 1.0))
                       (lambda(y) (- (* x x)))
                       k)
     (- x)))

 

練習1.40

;; Exercise 1.40
;; 牛頓法求解立方方程
#lang racket
(require "newtons-method.rkt")

;; 求解立方方程
(define (cubic-root a b c)
  ;; 定義cubic函式
  (define (cubic a b c)
    (lambda(x)
      (+ (* x x x)
         (* a x x)
         (* b x)
         c)))
  ;; 求解
  (newtons-method (cubic a b c) 1))

 

練習1.41

我天真地以為結果會是8+5=13,然而結果是21

;; Exercise 1.41
;; 執行兩次某函式
#lang racket

;; 執行兩次輸入函式
(define (double f)
  (lambda(x) (f (f x))))

;; inc
(define (inc x)
  (+ x 1))

(((double (double double)) inc) 5)

來展開一下

(double (double double))展開為((double double) (double double))

每一個(double double)展開是(lambda(x) (double (double x)))),將一個操作進行4次,就是將第二個操作進行4次

而第二個操作將inc進行4次,4*4就是16次inc

 

練習1.42

;; Exercise 1.42
;; 依次執行函式
#lang racket

;; 返回複合函式
(define (compose f g)
  (lambda(x) (f (g x))))

 

練習1.43

;; Exercise 1.43
;; 重複執行某函式
#lang racket
(require "compose.rkt")

;; 執行n次f
(define (repeated f n)
  (define (repeated-iter n res)
    (if (= n 0)
        res
        (repeated-iter (- n 1) (compose f res))))
  (repeated-iter n identity))

 

練習1.44

;; Exercise 1.44
;; smooth
#lang racket
(require "repeated.rkt")

;; 定義dx
(define dx 0.00001)

;; smooth
(define (smooth f)
  (define (avg a b c)
    (/ (+ a b c) 3))
  (lambda(x) (avg (f (- x dx)) (f x) (f (+ x dx)))))

;; n次smooth
(define (n-fold-smooth f n)
  ((repeated smooth n) f))

 

練習1.45

程式碼很好寫,關鍵在於如何根據n確定要使用average-dump的次數,雖然題目只是要求expertiment結果,網上也很多人做了,結果就是\log _2{n},我就偷懶了

;; Exercise 1.45
;; 求解n次方根
#lang racket
(require (file "../Procedures as General Methods/fixed-point.rkt"))
(require (file "../Exponentiation/fast-exp.rkt"))
(require "repeated.rkt")

;; average damp
(define (average-damp f)
  (lambda(x) (/ (+ (f x) x) 2)))

;; 根據n,求解需要進行多少次average dump才能使fixed-point收斂
(define (average-times n)
  ;; 迭代求解log2(n),p為計數器
  (define (iter res p)
    (if (> n res)
        (iter (* 2 res) (+ p 1))
        p))
  (iter 1 0))

;; 使用fixed-point進行n次方根求解
(define (nth-roots x n)
  (define (f y)
    (/ x (fast-exp y (- n 1))))
  (fixed-point ((repeated average-damp (average-times n)) f) 1.0))

為什麼average damp的次數是log_2{n},參見我的另一篇博文SICP習題1.45 為什麼做average damp的次數需要大於等於log2n

 

練習1.46

;; Exercise 1.46
;; 利用iterative improve求sqrt
#lang racket

;; 通用迭代improve
(define (iterative-improve good-enough? improve guess)
  (if (good-enough? guess)
      guess
      (iterative-improve good-enough? improve (improve guess))))

;; sqrt
(define (sqrt x)
  (define (good-enough? guess)
    (< (abs (- (* guess guess) x)) 0.0001))
  (define (improve guess)
    (/ (+ guess (/ x guess)) 2.0))
  (iterative-improve good-enough? improve x))