Fundamentals of Computer Science 1 (CS151 2003S)

Notes on Exam 2: Recursion

Contents

General Issues

Meaningless Operations

A number of you seem inclined to make some things much more complicated than they are. For example, I saw each of the following expressions in the exams that I graded:

The following would be much more sensible (primarily because people don't have to wonder why you're applying meaningless operations)

Formatting

Some of you still fail to write the six P's in an appropriate format, to choose good variable names, and to make sure that your lines are short enough. You lost points on this exam. You will lose many more on the next exam.

Conditionals

Repeat after me:

Problem 1: Counting Digits

Key Topics: Numeric Recursion

Document, write, and test a Scheme procedure, (count-appearances digit val), that counts how many times the one-digit integer digit appears in the non-negative integer value val.

For example,

> (count-appearances 1 12)
1
> (count-appearances 1 312)
1
> (count-appearances 1 10312101)
4
> (count-appearances 0 10312101)
2
> (count-appearances 5 10312101)
0
> (count-appearances 0 000100)
2

A Solution

This problem requires that we think about two aspects: How we look at digits of a number and what we'll recurse over. We'll repeatedly look at the last digit in the number and remove that digit.

How do we get the last digit of a number? We look at the remainder after dividing the number by 10. How do we shrink the number? We divide by 10. When do we stop? When the number equals 0.

Are there any special cases? Certainly. If the digit is 0 and the number is 0, we'll return 1. In order to distinguish that case from recursing until we reach 0, we'll build a kernel for the recursion. As a lover of named lets, I'm going to use one for the kernel.

;;; Procedure:
;;;   count-digits
;;; Parameters:
;;;   digit, an integer
;;;   num, a non-negative integer
;;; Purpose:
;;;   Counts the number of times digit appears in num.
;;; Produces:
;;;   count, a count of the number of appearances.
;;; Preconditions:
;;;   digit is a single-digit number (0, 1, 2, ..., 9).
;;;   num has the form d_n d_(n-1) d_(n-2) ... 3 2 1
;;; Postconditions
;;;   count is the number of d_i such that d_i = digit.
(define count-digits
  (lambda (digit num)
    (if (and (zero? digit) (zero? num))
        1
        (let kernel ((num num))
          (cond
            ((zero? num) 0)
            ((= digit (remainder num 10)) (+ 1 (kernel (quotient num 10))))
            (else (kernel (quotient num 10))))))))

An Alternative Solution

For those not happy to think about the mathematical side of things, it is also possible to convert the problem to that of tallying. In particular, we can convert the number to a string and then the string to a list of characters. Here's an attempt to do so.

(define count-digits-again
  (lambda (digit num)
    (let ((digit-char (car (string->list (number->string digit)))))
      (let kernel ((chars (string->list (number->string num))))
         (cond
           ((null? chars) 0)
           ((char=? digit-char (car chars)) (+ 1 (kernel (cdr chars))))
           (else (kernel (cdr chars))))))))

We've eliminated the special case, but at the cost of less elegance and some less understandable code.

Notes

I intended this to be a relatively straightforward question. In particular, the lab on numeric recursion included an example procedure that counted the number of digits in an integer. I expected that you'd be able to use a similar technique for this problem.

Problem 2: Determining Membership

Key Topics: List Recursion, Deep Recursion, Equality Testing, Predicates

Write and test a procedure, (in-tree? value tree-of-values), which determines whether the simple value value is either equal to tree-of-values or appears somewhere in tree-of-values, where tree-of-values is built by combining a number of cons cells (either explicitly, with cons or implicitly, with list).

You need not document this procedure.

> (in-tree? 0 null)
#f
> (in-tree? 0 0)
#t
> (in-tree? 0 1)
#f
> (in-tree? 0 (cons 1 1))
#f
> (in-tree? 0 (cons 0 1))
#t
> (in-tree? 0 (cons 1 0))
#t
> (in-tree? 0 (cons 1 (cons 0 2)))
#t
> (in-tree? 0 (list 0))
#t
> (in-tree? 0 (list 5 4 3 2 1 0))
#t
> (in-tree? 0 (list (list "one" 2 'three) (list 0) (list 2)))
#t

A Solution

Recall that trees are defined recursively: A tree is either (1) a non-pair value or (2) a pair of trees. Hence, our solution will be defined recursively. The base case is the non-pair value. We can determine if a value is in the simplest tree by comparing the two values. A value is in a pair of trees if it is in either tree.

Translating that into Scheme code:

;;; Procedure:
;;;   in-tree?
;;; Parameters:
;;;   val, a simple value
;;;   tree, a tree of values
;;; Purpose:
;;;   Determine if val appears in tree.
;;; Produces:
;;;   is-in-tree, a truth value.
;;; Preconditions:
;;;   [The Types Match]
;;; Postconditions:
;;;   is-in-tree is true (#t) if either (1) tree is a singleton value 
;;;   and val equals tree or (2) tree is a pair and val is in one of
;;;   the subtrees.
(define in-tree?
  (lambda (val tree)
    (if (pair? tree)
        (or (in-tree? val (car tree))
            (in-tree? val (cdr tree)))
        (equal? val tree))))

Problem 3: Converting Lists to Strings

Steven and Sarah Stringer find it fascinating that Scheme can figure out how to print such a wide variety of Scheme types. They find it particularly interesting that Scheme is able to print out lists without knowing their length in advance. They've asked you to show them some code that explains what Scheme does.

Write a procedure, (list-of-integers->string lst), which takes a list of integers and converts it to the corresponding string.

For example,

> (list-of-integers->string null)
"()"
> (list-of-integers->string (cons 1 null))
"(1)"
> (list-of-integers->string (list 1 2))
"(1 2)"
> (list-of-integers->string (cons 1 (cons 2 (list 5 4 3))))
"(1 2 5 4 3)"

You will probably find it useful to use the built-in number->string procedure. I expect that you will also want to create a helper procedure to deal with all but the first element of the list.

If you are feeling particularly ambitious (that is, this part of the problem is optional), you can handle nested lists and things built of cons cells that aren't necessarily lists.

A Solution

We need to print an open paren, the values separated by spaces, and the close paren. We'll use a kernel to print the values. The special cases to handle are the empty list and the singleton list (the two times we shouldn't include spaces).

;;; Procedure:
;;;   list-of-integer->string
;;; Parameters:
;;;   lst, a list of integers
;;; Purpose:
;;;   Convert a list of integers to a string.
;;; Produces:
;;;   str, a string
;;; Preconditions:
;;;   [None]
;;; Postconditions:
;;;   str is the string Scheme would print for lst.
(define list-of-integers->string
  (lambda (lst)
    (letrec (
             ; Convert a list of numbers to a string with the
             ; numbers separated by spaces.  Doesn't include parens.
             (kernel (lambda (lst)
                       (cond 
                         ((null? lst) "")
                         ((null? (cdr lst)) (number->string (car lst)))
                         (else (string-append (number->string (car lst))
                                              " "
                                              (kernel (cdr lst)))))))
            )
      (string-append "(" (kernel lst) ")"))))

Problem 4: Documenting Procedures

An and Al Abbrev object to the overly-long procedure names that Sam likes to use, like list-of-integers->string. Hence, they tend to choose one-character names. They also avoid the six P's that I like. Here's a procedure they've recently written.

(define r
  (lambda (l p?)
    (letrec ((c (lambda (p v)
                  (let ((x (if (p? v) 1 0)))
                    (cons (+ (car p) x) (+ (cdr p) (- 1 x))))))
             (r (lambda (q m)
                  (if (null? m) 
                      (/ (car q) (cdr q))
                      (r (c q (car m)) (cdr m))))))
      (r (cons 0 0) l))))

a. Change the various names in the procedure to clarify what the procedure does.

b. Add internal comments to explain the various parts.

c. Add introductory comments (the six P's) to explain the purpose (and the other P's) of the procedure.

Some Analysis

What do we observe?

A Solution

;;; Procedure:
;;;   ratio
;;; Parameters:
;;;   pred?, a unary predicate
;;;   lst, a list of values
;;; Purpose:
;;;   Compute the ratio of values for which pred? holds to values
;;;   for which pred? fails to hold.
;;; Produces:
;;;   rat 
;;; Preconditions:
;;;   pred? can be applied to every element of lst.
;;;   pred? fails to hold for at least one element of lst.
;;; Postconditions:
;;;   rat = holds/notholds where holds is the number of values in
;;;     lst for which (pred? val) is not false and notholds is
;;;     the number of values in lst for which (pred? val) is false.
;;;   rat is presented in simplest form.  For example, if pred? holds
;;;     for two values and fails to hold for four values, rat is
;;;     1/2 rather than 2/4.
(define ratio
  (lambda (lst pred?)
    (letrec ((count-one 
              (lambda (holds-notholds val)
                (let ((this-holds (if (pred? val) 1 0)))
                  (cons (+ (car holds-notholds) this-holds) 
                        (+ (cdr holds-notholds) (- 1 this-holds))))))
             (kernel (lambda (holds-notholds remaining)
                  (if (null? remaining) 
                      (/ (car holds-notholds) (cdr holds-notholds))
                      (kernel (count-one holds-notholds (car remaining)) 
                              (cdr remaining))))))
      (kernel (cons 0 0) lst))))

Some Notes

Although I did not take off, I was hoping that you would include the following two key preconditions:

 

History

Wednesday, 9 April 2003 [Samuel A. Rebelsky]

Thursday, 10 April 2003 [Samuel A. Rebelsky]

Friday, 11 April 2003 [Samuel A. Rebelsky]

 

Disclaimer: I usually create these pages on the fly, which means that I rarely proofread them and they may contain bad grammar and incorrect details. It also means that I tend to update them regularly (see the history for more details). Feel free to contact me with any suggestions for changes.

This document was generated by Siteweaver on Tue May 6 09:22:21 2003.
The source to the document was last modified on Fri Apr 11 13:08:06 2003.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2003S/Exams/notes.02.html.

Valid HTML 4.0 ; Valid CSS! ; Check with Bobby

Samuel A. Rebelsky, rebelsky@grinnell.edu