計算過程---SICP學習筆記(2)
一.線性遞迴和迭代
1.遞迴計算過程:一個計算過程構造起一個推遲進行的操作所形成的鏈條,而收縮階段表現為計算實際執行過程,這樣的計算過程稱為遞迴過程。
2.迭代計算過程:計算過程中沒有任何的增長或收縮,計算過程中的每一步的結果都需要儲存的軌跡,這樣的計算過程稱為迭代計算過程。
迭代計算過程和遞迴計算過程都在語法上使用到了遞迴過程,實則是有區別的。遞迴計算過程在使用遞迴過程呼叫自身時,包含著隱含的資訊,而未儲存在中間變數中,這和迭代所使用遞迴過程的用法是不同的。迭代過程往往在常見語言中使用(until,while,for 等),利用遞迴過程表達迭代計算過程往往也是以尾遞迴的形式出現的。
下面展現一個例子分別使用遞迴計算過程和迭代計算過程表達求x的階乘:
;用遞迴計算過程求x的階乘
(define (factorial x)
(if(= x 1)
1
(* x (factorial (- x 1)))))
;用迭代計算過程求x的階乘
(define (factorials n)
(factorial-iter 1 1 n)
)
(define (factorial-iter counter product n)
(if(> counter n)
product
(factorial-iter (+ counter 1) (* counter product) n)
)
)
我們可以看到在迭代計算過程中,呼叫遞迴過程總是在最後一句中,且只是遞迴呼叫,所以稱為尾遞迴。並且在該過程中,實際的計算過程是在遞迴呼叫過程中的引數中實現的,由此保留了計算過程的軌跡,所以稱之為迭代過程。
又可發現在遞迴計算過程中,儲存的資訊量與x的大小成正比關係,所以稱為線性遞迴計算過程。
二、樹形遞迴
樹形遞迴:在計算過程中每層遞迴呼叫都會分裂為兩個或兩個以上的多個分支。
例如斐波那契數列的遞迴計算過程:
(define (fib n)
(cond
((= n 0) 0)
((= n 1) 1)
(else(+ (fib (- n 1)) (fib (- n 2)))))
)
其呼叫次數是隨著n的增大而指數式增長的,其中:
相對於遞迴計算過程,迭代計算過程的開銷就小得多:
(define (fib-iter a b count)
(if(= count 0)
b
(fib-iter (+ a b) a (- count 1))))
(define (fibs n)
(fib-iter 1 0 n))
該過程就是一個線性迭代的過程
三、遞迴計算過程例項:換零錢方式的統計
問題:考慮有下列幣制:50塊、25塊、10塊、5塊和1塊,輸入任意金額求的所有換取零錢的方法數量總和。
思路:將數量為a的現金換取為不同幣值的零錢方法等於:
- 將現金a換取為除了第一種幣值之外所有其他硬幣的方式數量總和。
- 將現金a-d換取為所有種類幣值的方式數量總和,其中d是第一種幣值。
第一組方式中全都不含有幣值x,而第二組中必會包含至少一個幣值x,因此全不包含一種幣值加上全都包含一種幣值的方法數量總和就是全部的方法數量的總和。
程式碼如下:
(define (change cash)
(cc cash 5))
(define (cc cash kind-of-coins)
(cond ((= cash 0) 1)
((or (< cash 0) (= kind-of-coins 0)) 0)
(else (+ (cc cash (- kind-of-coins 1))
(cc (- cash (coins-kind kind-of-coins)) kind-of-coins)))))
(define (coins-kind kind-of-coin)
(cond ((= kind-of-coin 1) 1)
((= kind-of-coin 2) 5)
((= kind-of-coin 3) 10)
((= kind-of-coin 4) 25)
((= kind-of-coin 5) 50)))
輸入100元:(change 100)
得到292種方法。
四、求冪
繼續來探討遞迴計算過程和迭代計算過程
根據求冪的公式:
可以得到遞迴版本:
(define (expt x n)
(if (= n 0) 1
(* x (expt x (- n 1)))))
相應的也有等價的迭代版本。
或者可以根據公式:
得到更加快速的求冪版本:
;遞迴計算過程
(define (square x) (* x x))
(define (even x)
(= (remainder x 2) 0))
(define (fast-expt x n)
(cond ((even x) (square (fast-expt x (/ n 2))))
((= n 0) 1)
(else (* x (fast-expt x (- n 1))))))
同樣地,再次變換公式求法,可以改變迭代計算過程的實現方式
(define (fast-expt-i x count)
(if (= (remainder count 2) 1)
(* x (fast-iter x (- count 1) 1))
(fast-iter x count 1)))
(define (fast-iter x count product)
(cond ((= count 1) product)
(else (fast-iter x (/ count 2) (* (square x) product)))))
五、例項:素數檢測
思路:首先使用最平凡的方式實現素數檢測,即:從2開始累加,求得x的最小因子,若最小因子是x本身,則表明該數是素數。
(define (square x) (* x x))
(define (smallest-divisor n)
(get-divisor n 2))
(define (get-divisor find test)
(cond ((> (square test) find) find)
((= (remainder find test) 0) test)
(else (get-divisor find (+ test 1)))))
(define (IsPrime x)
(= (smallest-divisor x) x))
(smallest-divisor 199)
測得199的最小因子是199所以是素數。
下面使用費馬小定理來檢測素數,該方法可使得計算過程步數的增長階減小為對數的。
費馬小定理:如果n是一個素數,a是小於n的任意一個正整數,那麼a的n次方與a模n同餘。
實現如下:
(define (expmod x exp m) ;求得x的n次方並且模m的數
(cond ((= exp 0) 1)
((= (remainder exp 2) 0)
(remainder (square(expmod x (/ exp 2) m)) m)) ;使用前面的平方求冪
(else (remainder (* x (expmod x (- exp 1) m)) m))))
(define (fermat-test n) ;費馬檢查
(define (test a)
(= (expmod a n n) a)
)
(test (+ 1 (random (- n 1))))) ;隨機抽取範圍為1-(n-1)的數字
(fermat-test 6)
(fermat-test 561)
在求x的n次方並且模m的過程中,並不是先求得x的n次方再對m求模,而是對遞迴計算過程中每一個冪結果求一次模,與前者方法等價。
費馬檢查的結果只能滿足概率上的正確性,數字不通過費馬檢查的絕對不是素數,而通過費馬檢查數仍有可能是素數,這樣的數稱之為Carmichael數,1-100000000內有255個這樣的數字。