一.线性递归和迭代
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的增大而指数式增长的,其中:
Fib(n)=ϕ25√
相对于递归计算过程,迭代计算过程的开销就小得多:
(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种方法。
四、求幂
继续来探讨递归计算过程和迭代计算过程
根据求幂的公式:
bn=b⋅bn−1
可以得到递归版本:
(define (expt x n)
(if (= n 0) 1
(* x (expt x (- n 1)))))
相应的也有等价的迭代版本。
或者可以根据公式:
bn=(bn2)2
得到更加快速的求幂版本:
;递归计算过程
(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))))))
同样地,再次变换公式求法,可以改变迭代计算过程的实现方式
(b2)n2=(bn2)2
(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个这样的数字。