1.2 Procedures and the Processes They Generate
To become experts, we must learn to visualize the processes generated by various types of procedures. Only after we have developed such a skill can we learn to reliably construct programs that exhibit the desired behavior. (Section 1.2)
- A procedure is a pattern for the local evolution of a computation process: how one stage is built on the previous stage.
- The global behavior of a computational process is much harder to reason about.
- Processes governed by different types of procedures generate different “shapes.”
- Computational processes consume two important resources: time and space.
# 1.2.1 Linear Recursion and Iteration
- The factorial of is defined as the product of the integers on the interval
- The naive recursive implementation creates a curved shape:
(factorial 4)
(* 4 (factorial 3))
(* 4 (* 3 (factorial 2)))
(* 4 (* 3 (* 2 (factorial 1))))
(* 4 (* 3 (* 2 1)))
(* 4 (* 3 2))
(* 4 6)
24
- The iterative implementation maintains a running product and multiplies the numbers from 1 to This creates a shape with a straight edge:
(factorial 4)
(fact-iter 1 1 4)
(fact-iter 1 2 4)
(fact-iter 2 3 4)
(fact-iter 6 4 4)
(fact-iter 24 5 4)
24
- Both compute the same mathematical function, but the computational processes evolve very differently.
- The first one is a linear recursive process. The chain of deferred operations causes an expansion (as operations are added) and a contraction (as operations are performed).
- The interpreter must keep track of all these operations.
- It is a linear recursive process because the information it must keep track of (the call stack) grows linearly with
- The second is a linear iterative process. It does not grow and shrink.
- It is summarized by a fixed number of state variables and a rule to describe how they should update and when the process should terminate.
- It is a linear iterative process because the number of steps grows linearly with
- In the iterative process, the variables provide a complete description of the state of the process at any point. In the recursive process, there is “hidden” information that makes it impossible to resume the process midway through.
- The longer the chain of deferred operations, the more information must be maintained (in a stack, as we will see).
- A recursive procedure is simply a procedure that refers to itself directly or indirectly.
- A recursive process refers to the evolution of the process described above.
- A recursive procedure can generate an iterative process in Scheme thanks to tail-call optimization. In other languages, special-purpose looping constructs are needed.
# 1.2.2 Tree Recursion
- With tree recursion, the procedure invokes itself more than once, causing the process to evolve in the shape of a tree.
- The naive Fibonacci procedure calls itself twice each time it is invoked, so each branch splits into two at each level.
In general, the number of steps required by a tree-recursive process will be proportional to the number of nodes in the tree, while the space required will be proportional to the maximum depth of the tree. (Section 1.2.2)
- The iterative Fibonacci procedure is vastly more efficient in space and in time.
# Example: Counting change
Let represent the number of ways of changing the amount using kinds of coins. If the first kind of coin has denomination then In words, there are two situations: where you do not use any of the first kind of coin, and when you do. The value of assumes we don’t use the first kind at all; the value of assumes we use one or more of the first kind.
That rule and a few degenerate cases are sufficient to describe an algorithm for counting the number of ways of changing amounts of money. We can define it with the following piecewise function:
Like Fibonacci, the easy tree-recursive implementation involves a lot of redundancy. Unlike it, there is no obvious iterative solution (it is possible, just harder). One way to improve the performance of the tree-recursive process is to use memoization.
# 1.2.3 Orders of Growth
- Different processes consume different amounts of computational resources.
- We compare this using order of growth, a gross measure of the resources required by a process as the inputs becomes larger.
- Let be a parameter that measures the size of a problem—it could be the input itself, the tolerance, the number of rows in the matrix, etc.
- Let be the amount of resources the process requires for a problem of size This could be time, space (amount of memory), number of registers used, etc.
- We say that has order of growth or if there are positive constants and independent of such that for any sufficiently large value of
- The value is sandwiched between and
- The linear recursive process for computing factorials had time and space (both linear), whereas the linear iterative process had space (constant).
- The order of growth is a crude description of the behavior of a process.
- Its importance is allowing us to see the change in the amount of resources required when you, say, increment or double
# 1.2.4 Exponentiation
One way to calculate to the power is via the following recursive definition:
A faster method is to use successive squaring:
# 1.2.5 Greatest Common Divisors
- The GCD of integers and is the largest integer that divides both and with no remainder. For example,
- An efficient algorithm uses
- For example, we can reduce
(gcd 206 40)
as follows:
(gcd 206 40)
(gcd 40 6)
(gcd 6 4)
(gcd 4 2)
(gcd 2 0)
2
- This always works: you always get a pair where the second number is zero, and the other number is the GCD of the original pair.
- This is called Euclid’s Algorithm.
- Lamé’s Theorem: If Euclid’s Algorithm requires steps to compute the GCD of some pair then
# 1.2.6 Example: Testing for Primality
# Searching for divisors
- One way to test for primality is to find the number’s divisors.
- A number is prime if and only if it is its own smallest divisor.
# The Fermat test
The Fermat test is a primality test based on Fermat’s Little Theorem:
If is a prime number and is any positive integer less than then raised to the power is congruent to modulo (Section 1.2.6)
The test works like this:
- Given a number pick a random number and calculate
- Fail: If the result is not equal to then is not prime.
- Pass: If the result is equal to then is likely prime.
- Repeat. The more times the number passes the test, the more confident we are that is prime. If there is a single failure, is certainly not prime.
# Probabilistic methods
- Most familiar algorithms compute an answer that is guaranteed to be correct.
- Not so with the Fermat test. If passes the Fermat test for one random value of all we know is that there is a better than 50% chance of being prime.
- A probabilistic algorithm does not always give a correct result, but you can prove that the chance of error becomes arbitrarily small.
- We can make the probability error in our primality test as small as we like simply by running more Fermat tests—except for Carmichael numbers.
Numbers that fool the Fermat test are called Carmichael numbers, and little is known about them other than that they are extremely rare. … In testing primality of very large numbers chosen at random, the chance of stumbling upon a value that fools the Fermat test is less than the chance that cosmic radiation will cause the computer to make an error in carrying out a “correct” algorithm. Considering an algorithm to be inadequate for the first reason but not for the second illustrates the difference between mathematics and engineering. (Footnote 1.47)