Filtering
With the help of recursion, we can perform many common operation on lists: filtering. One might, for example, write a procedure that takes any list of real numbers and returns a similar list, but with all the negative numbers removed; such a procedure would ``filter out'' the negative numbers. Here's how it would look:
(define filter-out-negatives (lambda (ls) (cond ((null? ls) '()) ((negative? (car ls)) (filter-out-negatives (cdr ls))) (else (cons (car ls) (filter-out-negatives (cdr ls)))))))
In English: If the list ls
is empty, return the empty
list. Otherwise, if the first element on the list is negative, discard it
and proceed to apply the filter to the rest of the list. But if the first
element on the list is zero or positive, add it to the front of the list
obtained by applying the filter to the rest of the list.
Start Scheme and enter the definition of filter-out-negatives
.
Confirm that it correctly filters out negatives from any list of numbers.
What happens if all of the elements of the list are negative? What happens
if ls
contains symbols instead of numbers?
Write and test a Scheme procedure filter-outliers
that takes a
list of real numbers and filters out those that are not in the range from 0
to 100.
(filter-outliers '(93 86 92 -3 100 81 77 84)) ===> (93 86 92 100 81 77 84) (filter-outliers '(-50 0 50 100 150)) ===> (0 50 100) (filter-outliers '(1000000)) ===> () (filter-outliers '()) ===> ()
Processing Number Sequences
Recursion can be used simply to run through a series of numerical values, so that each of those values can be operated on by a fixed procedure. For instance, the following procedure computes the sum of the squares of the positive integers from 1 to n, for any non-negative integer n:
(define sum-of-squares (lambda (n) (if (zero? n) 0 (+ (square n) (sum-of-squares (- n 1))))))
Adapt this procedure so that squares of odd numbers are excluded from the sum.
Counting Parameters
In other cases, the numerical parameter is simply a counter, tallying the
number of repetitions of some operation that may not involve numbers at
all. For instance, we might want procedure dupl
that takes
two arguments, a string str
and a non-negative integer
factor
, and returns a string consisting of factor
successive copies of str
:
(dupl "alf" 4) ===> "alfalfalfalf" (dupl "What? " 5) ===> "What? What? What? What? What? " (dupl "*-" 10) ===> "*-*-*-*-*-*-*-*-*-*-" (dupl "" 153) ===> "" (dupl "invisible" 0) ===> ""
Here's how we'd write it:
(define dupl (lambda (str factor) (if (zero? factor) "" (string-append str (dupl str (- factor 1))))))
The string-append
operation itself deals only with strings,
not with numbers; the role of factor
is simply to count off
the number of repetitions remaining and to cut off the recursion once this
number decreases to zero.
Write and test a procedure dupl-to-fit
, similar to
dupl
, except that the second parameter indicates the maximum
length permitted for the result string -- str
should be
duplicated as many times as possible without exceeding this maximum length.
(In other words, the recursion should terminate as soon as the amount of
``free space'' left for the result string is less than the length of
str
.)
(dupl-to-fit "alf" 11) ===> "alfalfalf" (dupl-to-fit "alf" 12) ===> "alfalfalfalf" (dupl-to-fit "*" 7) ===> "*******" (dupl-to-fit "*-" 7) ===> "*-*-*-" (dupl-to-fit "invisible" 3) ===> ""
Be careful not to use the null string as the first argument to this procedure!
Finding Roots Using the Bisection Method
Suppose we are given a continuous function f, and we want to approximate a value r where f(r)=0. While this can be a difficult problem in general, suppose that we can guess two points a and b (perhaps from a graph) where f(a) and f(b) have opposite signs. The four possible cases are shown below:
Given a and b, we know that a root r must lie between them, since f(a) and f(b) have opposite signs. Thus, we know that r must lie in the interval [a, b]. In one step, we can cut this interval in half as follows. If f(a) and f(m) have opposite signs, then r must lie in the interval [a, m]; otherwise, r must lie in the interval [m, b].
As with the discussion of the cube-root program in Section 2.2, once one understands how to halve the possible interval for r in a step, then one can apply this process recursively to halve the interval as much as one wants from an initial interval [a, b].
To apply this approach in an actual program, a few additional details are
needed. First, one must decide how to specify the function f. For
now, this could be accomplished by writing a separate definition, such as
(define f ...)
. For example, one might use
(define f (lambda (x) (- (* x x x) 2)))
Third, one must decide what values to use for a and b. It may be best to let the user enter these values as parameters.
Fourth, one must consider how to decide if two real numbers (u and v) have the opposite sign. One approach is to use cases -- just as in the above figure. A second approach is to note that the numbers have the opposite sign if their product is negative.
Apply your procedure to find the cube root of 2.
Change f to find the cube root of 8, which should be 2. Does your procedure root work in this case? If not, make any needed corrections.
Then, change f to sin(x) and find the root between 2 and 4. (Since the actual root is Pi, this will give you an approximation to that value.)
This document is available on the World Wide Web as
http://www.math.grin.edu/~walker/courses/153.sp00/lab-recursive-design.html