一、求取最大公因数的欧几里得算法

算法思想基于以下观察,如果r是a除以b的余数,那么a和b的公约数正好也是b和r的公约数,用等式表示:

GCD(a,b) = GCD(b,r)

GCD : greatest common divisor,最大公因数(公约数)

这就把一个求取最大公因数的问题连续地归约到对越来越小的整数对求最大公因数的计算问题。例如:

GCD(206,40) = GCD(40,6)
			= GCD(6,4)
			= GCD(4,2)
			= GCD(2,0)
			= 2

将GCD(206,40)归约到GCD(2,0),最终得到2。
上述这种计算最大公因数到方法称为欧几里得算法

不难将欧几里得算法写成一个过程:

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

这将产生一个迭代计算过程,其步数依所涉及的数的对数增长。

尝试把上述过程直译为中文:

(定义(求取最大公因数的欧几里得算法 甲 乙)
    (如果 (= 乙 0)
          甲
          (求取最大公因数的欧几里得算法 乙 (取余数 甲 乙))))

二、素数检测

1.寻找因子

判断一个数是否为素数的最朴素的方法便是从2开始,一个个地试,看有没有哪个整数能够整除这个数。

下面的程序能找出给定数n的(大于1的)最小整数因子:

(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是素数当且仅当它是自己的最小因子:

(define (prime? n)
    (= n (smallest-divisor n)))

判断一个未知数n是否是素数,试除数时只需从2试到SchemaNames schemaname是什么意思_最大公因数即可,对于这一事实,你可以这么简单来想:一个数如果不是素数,是合数,那么它的因数必然有一个小于等于SchemaNames schemaname是什么意思_最大公因数(大于等于2),相应的另一个因数大于等于SchemaNames schemaname是什么意思_最大公因数。因此只需从2试到SchemaNames schemaname是什么意思_最大公因数就行了。

尝试把上述程序直译为中文:

(定义 (最小因数 元)
    (找寻最小因数 元 2))
(定义 (找寻最小因数 元 除数))
    (情况符合 ((> (平方 除数) 元) 元)
             ((整除? 除数 元) 除数)
             (其它情况 (找寻最小因数 元 (+ 除数 1)))))
(定义 (整除? 除数 被除数)
    (= (取余数 被除数 除数) 0)

(定义 (质数? 元)
    (= 元 (最小因数 元)))

2.费马检查

费马小定理:如果n是一个素数,a是小于n的任意正整数,那么a的n次方与a模n同余(除以n余数相同)

根据费马小定理的内容,可以给出一种判断数n是否为素数的思路。即:对于给定的整数n,随机任取一个a<n并计算出an取模n的余数。如果得到的结果不等于a,那么n就肯定不是素数,如果就是a,那么n是素数的机会就很大。再另取一个随机的a并采用同样方式检查。如果它满足上述等式,那么我们就能对n是素数有更大的信心另。通过检查越来越多的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))))

尝试把上述过程直译为中文:

(定义 (一个数的幂对另一个数取模 底数 指数 元)
	(情况符合 ((= 指数 0) 1)
	         ((偶数? 指数)
	          (取余数(平方(一个数的幂对另一个数取模 底数 (/ 指数 2) 元))
	                  元))
	         (其它情况
	          (取余数(* 底数 (一个数的幂对另一个数取模 底数 (- 指数 1) 元))
	                     元))))

执行费马检查需要选取位于1和n-1之间(包含这两者)的数a,而后检查a的n次幂取模n的余数是否等于a。随机数a的选取通过过程random完成,我们假定它已经包含再Scheme的基本过程中,它返回比其整数输入小的某个非负整数。这样,要得到1和n-1之间的随机数,只需要输入n-1去调用random,并将结果加1:

(define (fermat-test n)
    (define (try-it a)
        (= (expmod a n n) a))
    (try-it (+ 1 (random (- n 1))))

尝试把上述过程直译为中文:

(定义 (费马检查 元)
    (定义 (尝试 甲)
        (= (一个数的幂对另一个数取模 甲 元 元) 甲))
    (尝试 (+ 1 (随机数 (- 元 1))))

给一个次数参数,如果每次检查都成功,过程的值就是真,否则就是假:

(define (fast-prime? n times)
    (cond ((= times 0) true
          ((fermeat-test n) (fast-prime? n (- times 1)))
          (else false)))

尝试把上述过程直译为中文:

(定义 (快速判断是否为质数? 元 次数)
    (情况符合 ((= 次数 0) 是)
             ((费马检查 元) (快速判断是否为质数? 元 (- 次数 1)))
             (否则 否)))

概率方法:
费马检查得到的结果只有概率上的正确性。说得更准确写,如果数n不能通过费马检查,我们可以确信它一定不是素数。而n通过了这一检查的事实只能作为它是素数的一个很强的证据,但却不是对n为素数对保证。我们能说的是,对于任何数n,如果执行这一检查的次数足够多,而且看到n通过了检查,那么就能使这一素数检查出错的概率减小到所需要的任意程度。
注:确实存在一些能骗过费马检查的整数:某些数n不是素数但却具有这样的性质,但这样的数极少。
能够证明,存在着使这样的出错机会达到任意小的检查算法,激发了人们对这类算法的极大兴趣,已经形成了人所公知称为概率算法的领域。在这一领域中已经有了大量研究工作,概率算法也已被成功地应用于许多重要领域。(比如密码学中的RSA算法)

练习1:
使用smallest-divisor过程找出下面各数的最小因子: 199. 1999. 19999。

SchemaNames schemaname是什么意思_最大公因数_05


SchemaNames schemaname是什么意思_计算机程序的构造和解释_06


SchemaNames schemaname是什么意思_最大公因数_07


代码本体:

(define (square a)
    (* a a))

(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))

(display (smallest-divisor 19999))
(exit)

尝试把上述程序直译为汉语:

(定义 (平方 甲)
    (* 甲 甲))

(定义 (最小因数 元)
    (找寻最小因数 元 2))

(定义 (找寻最小因数 元 除数)
    (情况符合 ((> (平方 除数) 元) 元)
             ((整除? 除数 元) 除数)
             (否则 (找寻最小因数 元 (+ 除数 1)))))

(定义 (整除? 除数 被除数)
    (= (取余数 被除数 除数) 0))

(输出 (最小因数 19999))
(退出)

练习2:
大部分Lisp 实现都包含一个runtime基本过程,调用它将返回一个整数,表示系统已经运行的时间(例如, 以微秒计)。在对整数n调用下面的timed-prime-test过程时,将打印出n并检查n是否为素数。如果n是素数,过程将打印出三个星号,随后是执行这一检查所用的时间量。

(define (timed-prime-test n)
    (newline)
    (display n)
    (start-prime-test n (runtime)))
(define (start-prime-test n start-time)
    (if (prime? n)
        (report-prime (- (runtime) start-time))))
(define (report-prime elapsed-time)
    (display " *** ")
    (display elapsed-time))

请利用这–过程写一个search-for-primes过程,它检查给定范围内连续的各个奇数的素性。请用你的过程找出大于1 000、大于10 000、大于100000和大于1 000 000的三个最小的素数。请注意其中检查每个素数所需要的时间。因为这一检查算法具有 SchemaNames schemaname是什么意思_取模_08 的增长阶,你可以期望在10000附近的素数检查的耗时大约是在1000附近的素数检查的SchemaNames schemaname是什么意思_Lisp_09倍。你得到的数据确实如此吗?对于100 000和1 000 000得到的数据,对这一SchemaNames schemaname是什么意思_SchemaNames_10预测的支持情况如何?有人说程序在你的机器上运行的时间正比于计算所需的步数,你得到的结果符合这种说法吗?

参考文献:
[1] [美]Julie Sussman.计算机程序的构造和解释[M]. 裘宗燕译注.北京:机械工业出版社,1996.