Laboratory Exercises For Computer Science 153

Designing Recursive Procedures

Designing Recursive Procedures

Goals: This laboratory exercise provides experience using recursion to solve a variety of types of problem.

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.

  1. 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?

  2. 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))))))
  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.

  1. 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:

4 Cases for the Bisection Method

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)))
  1. Explain briefly why finding the root of this function will produce the cube root of 2.
Second, one must decide how long to continue. One approach is to keep the recursion going as long as the interval is longer than a length epsilon.

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.

  1. Write a procedure root that uses the Bisection Method to find a root of the function f, given (parameter) values for a and b. In your program, define epsilon as 0.00005, and continue until you have narrowed down the root to an interval at least that small. (Returning the midpoint of the interval then could give a reasonable approximation for r.)

    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

created February 5, 1997 by John David Stone
completely revised January 7, 2000 by Henry M. Walker