Section 1.2.4
Exercise 1.16
This is the fast recursive version, for reference:(defun square (x) (* x x)) ; Recursive version (defun fast-expt (b n) (cond ((= n 0) 1) ((evenp n) (square (fast-expt b (/ n 2)))) (t (* b (fast-expt b (- n 1))))))
And this is the iterative version, the requirement of the exercise. Note my usage of the &optional
function parameter for a
. Common Lisp supports optional and keyword parameters, which makes writing functions more convenient sometimes.
; Iterative version ; Note: using CL's &optional arguments instead ; of the auxiliary function ; (defun fast-expt-iter (b n &optional (a 1)) (cond ((= n 0) a) ((evenp n) (fast-expt-iter (square b) (/ n 2) a)) (t (fast-expt-iter b (- n 1) (* b a)))))
Exercise 1.17
I will implement double
and halve
using normal CL operators. If our “machine” really doesn’t have multiplication instructions, the doubling and halving can be implemented by means of left and right shift, respectively.
(defun double (x) (* x 2)) (defun halve (x) (/ x 2)) (defun fast-mult (a b) (cond ((= b 0) 0) ((= b 1) a) ((evenp b) (double (fast-mult a (halve b)))) (t (+ a (fast-mult a (- b 1))))))
Note that there are two special cases – for 0 and for 1. The case (= b 0)
will not be reached during the recursion – it’s only there to provide an answer when (fast-mult a 0)
is requested by the user.
Exercise 1.18
After understanding the technique of solution in exercise 1.16, this one is quite simple. The auxuliary state variable is acc
– it accumulates the sum, and the invariant answer = a*b + acc
holds on each iteration.
(defun fast-mult-iter (a b &optional (acc 0)) (cond ((= b 0) acc) ((evenp b) (fast-mult-iter (double a) (halve b) acc)) (t (fast-mult-iter a (1- b) (+ a acc)))))
Exercise 1.19
Tpq(a,b) = {a <- bq + aq + ap {b <- bp + aq
To apply Tpq
twice, we replace a
and b
on the right hand side in the equations above with their definitions by Tpq
(that is, with the full expansion).
Tp'q'(a,b) = {a <- bpq + aqq + bqp + aqp + app + bqq + aqq + apq {b <- bpp + aqp + bqq + aqq + apq = {a <- (pq + qq + qp)*b + (pq + qq + qp)*a + (qq + pp)*a = {b <- (pp + qq)*b + (pq + qp + qq)*a
Restructured this way, we see that indeed the double transformation works, and we get Tpq
back if we define:
p' = pp + qq q' = pq + qp + qq
Now we are ready to write the code:
(defun fast-fib-iter (n) (fast-fib-iter-aux 1 0 0 1 n)) (defun fast-fib-iter-aux (a b p q count) (cond ((= count 0) b) ((evenp count) (fast-fib-iter-aux a b (+ (* p p) (* q q)) ; p' (+ (* 2 p q) (* q q)) ; q' (/ count 2))) (t (fast-fib-iter-aux (+ (* b q) (* a q) (* a p)) (+ (* b p) (* a q)) p q (1- count)))))
Section 1.2.5
The authors present Euclid’s Algorithm for computing the GCD (Greatest Common Divisor) of two numbers. It’s a very interesting and important algorithm, worth to understand. Here is a simple proof I lifted from TAOCP section 1.11
GCD(a, b) The remainder of the division a/b is r. We can write this as: qb + r = a (for some integer q - the quotient of the division) This means that any number that divides b and r must also divide a. The equation can also be rewritten as: r = a - qb This means that any number that divides a and b must also divide r. So, the set of divisors of b and r is the same as the set of divisors of a and b. In particular, the greatest common divisor is the same. Q.E.D.
Also, the Lamé theorem is presented – it provides a fascinating connection between Euclid’s algorithm and Fibonacci numbers, and this is used to prove the the runtime of Euclid’s algorithm is logarithmic.
Exercise 1.20
We get back to applicative vs. normal order evaluation. In applicative order, the process generated is as follows (the number following a comment counts actual executions of remainder
):
(gcd 206 40) (gcd 40 (remainder 206 40)) ; 1 (gcd 40 6) (gcd 6 (remaider 40 6)) ; 2 (gcd 6 4) (gcd 4 (remainder 6 4)) ; 3 (gcd 4 2) (gcd 2 (remainder 4 2)) ; 4 (gcd 2 0) 2
So, remainder
is called 4 times, exactly in the sequence given in the example process. By ->
I mean that a call to gcd
occurs.
(gcd 206 40) (gcd 40 (remainder 206 40)) -> (if (= (remainder 206 40) 0) ; 1 time 40 (gcd (remainder 206 40) (remainder 40 (remainder 206 40)))) -> (if (= (remainder 40 (remainder 206 40)) 0) ; 2 times (remainder 206 40) (gcd (remainder 40 (remainder 206 40)) (remainder (remainder 206 40) (remainder 40 (remainder 206 40))))) -> if (= (remainder (remainder 206 40) (remainder 40 (remainder 206 40))) 0) ; 4 times (remainder 40 (remainder 206 40)) (gcd (remainder (remainder 206 40) (remainder 40 (remainder 206 40))) (remainder (remainder 40 (remainder 206 40)) (remainder (remainder 206 40) (remainder 40 (remainder 206 40))))) -> if (= (...)) ; 7 times (b from above) And this indeed equals 0, so the next is evaluated (a from above): (remainder (remainder 206 40) (remainder 40 (remainder 206 40))) Another 4 calls. In total 1 + 2 + 4 + 7 + 4 = 18 calls to remainder
Footnotes
1 Donald Knuth’s The Art Of Computer Programming.