Skip to main content

Exam 3: Recursion and related Racket

Assigned
Wednesday, 1 May 2019
Due
Tuesday, 7 May 2019 by 10:30pm
Collaboration
You may not receive help from any person other than the instructor for this exam. You may not assist others in doing this exam. See the exam procedures for a detailed description of the resources you can and cannot use.
Submitting
Rename the starter code file to 000000.rkt, but replace 000000 with your generated random number. Email this file as an attachment to the instructor when you are finished with your exam.

Please read the exam procedures page for policies, turn-in procedures, and grading details. If you have any questions about the exam, check the Q&A at the bottom of this page. We will generally spend some time in class every day on questions and answers while the exam is in progress.

While the exam is out, please check back periodically to see if we have reported any new errata.

Complete the exam using the exam03.rkt starter source code. Please rename this file to 000000.rkt, but replace 000000 with your generated random number.

Prologue

The prologue for this examination will be via email, which you should send to your instructor by 10:30 p.m. on Friday evening. Your message should be titled CSC 151.01: Exam 3 Prologue (your name).

  1. For each problem, please include a short note about something that will help you solve the problem. Mostly, we want to see some evidence that you’ve thought about the problem. You might note some similar procedures you’ve written or problems you’ve solved in the past (e.g., in a lab or on a homework assignment). You might note procedures that you expect to use. You might sketch an algorithm. You might pose a question to yourself. (We won’t necessarily read this in a timely fashion, so if you have questions for your instructor, you should ask by email or in person.)

    If, when looking at a problem, you think you already know the answer, you can feel free to write something short like “solved” or “trivial”.

  2. Which of those problems do you expect to be the most difficult for you to solve? Why?

  3. Conclude by answering the question What is an approach that you expect will help you be successful on this exam? For example, you might suggest that you will work thirty minutes on the exam each day, or work on the exam at 7pm each day, when your brain is most able to process information.

Epilogue

The epilogue for this examination will also be via email, which you should send to your instructor by 10:30 p.m. on the evening after the exam is due. Your message should be titled CSC 151.01: Exam 3 Epilogue (your name). Include answers to the following questions.

What was the most difficult part of the exam?

What made that part difficult?

What are two things that you would recommend to next semesters’ CSC 151 students to be successful on exams?

Problem 1: Palindromes

Topics: numeric recursion, strings and characters

A string is called a “palindrome” if reversing the characters of the string results in the same string. A few examples of palindromes are "radar", "civic", and "aabaa". Write, but do not document, a procedure (palindrome? str) that checks to see if the string str is a palindrome. You may not use any string comparison procedures for this problem (e.g. string=?, string<=?, etc.) nor may you build other strings along the way (e.g., you cannot call string-reverse or substring). You must leave the string as a string. You may not, for example, turn it into a list of characters.

You will find string-ref, string-length, and char=? useful procedures for solving this problem.

> (palindrome? "hello")
#f
> (palindrome? "civic")
#t
> (palindrome? "civics")
#f
> (palindrome? "Civic")
#f
> (palindrome? "abccba")
#t
> (palindrome? "")
#t

Hint: Use numeric recursion with the position of the character.

Problem 2: Computing square roots

Topics: numeric recursion, divide-and-conquer

Throughout the semester, you’ve used the sqrt procedure, which computes the square root of a positive real number. But what if that procedure did not exist? We’d need to implement it. Fortunately, we can use a divide-and-conquer algorithm to find square roots.

Start with two guesses: One of which you know is less than the square root of the number (0 is a good start), the other of which is larger than the square root of the number (the number itself is a good start). We repeatedly use the average of those two numbers as a guess and determine whether (a) the guess is close enough to the square root; (b) the guess is smaller than the square root; or (c) the guess is greater than the square root.

How do we know whether it’s close enough? The square root might be an irrational number, so we can’t expect to compute it exactly. So, we’ll consider an additional parameter, epsilon. If the square of the guess is within epsilon of the original number, we allow the computation to stop.

But what if the guess isn’t close enough? If the square of the guess is smaller than the original number, then our guess was too small, so we increase the lower bound. Otherwise, we decrease the upper bound. In both cases, we try again.

For example, let’s find the square root of 2 with an epsilon of 0.001.

Iteration 1

  • Our initial lower bound is 0 and upper bound is 2.
  • Our guess is the average of these two numbers, or 1.
  • 1 squared is 1.
  • (2 - 1) is 1, which is much bigger than 0.001, so we keep searching.
  • 1 is less than 2, so we increase our lower bound to our guess of 1.

Iteration 2

  • The current lower bound is 1 and upper bound is 2.
  • Our guess is 1.5, the average of 1 and 2.
  • 1.5 squared is 2.25.
  • (2.25 - 2) is .25, which is still bigger than 0.001, so we keep searching.
  • 2.25 is bigger than 2, so we decrease our upper bound to 1.5.

Iteration 3

  • The lower bound is now 1 and the upper bound is now 1.5.
  • Our guess is the average of these two numbers, 1.25.
  • 1.25 squared is 1.5625.
  • (2 - 1.5625) is 0.4375, which is still more than 0.001. (Note that our error does not always decrease. 1.25 is further from the correct answer than 1.5 was.)
  • 1.5625 is smaller than 2, so we increase our lower bound to our guess of 1.25.

The computation continues as follows.

  • Lower bound: 1.25; Upper bound: 1.5; Guess: 1.375; Square: 1.890625
  • Lower bound: 1.375; Upper bound: 1.5; Guess: 1.4375; Square: 2.06640625
  • Lower bound: 1.375; Upper bound: 1.4375; Guess: 1.40625; Square: 1.9775390625
  • Lower bound: 1.40625; Upper bound: 1.4375; Guess: 1.421875; Square: 2.021728515625
  • Lower bound: 1.40625; Upper bound: 1.421875; Guess: 1.4140625; Square: 1.99957275390625

Not a perfect result, but a fairly good estimate. Certainly, the square is within .001 of the expected value.

Now it’s your turn to turn this idea into code.

Document and write a procedure, (square-root n epsilon), that computes the square root of n using the technique described above.

In writing this procedure, you may assume that n is at least 2.

Note: You will find it useful to have a kernel that keeps track of the lower and upper bound described above.

Problem 3: Riffling lists

Topics: list recursion

As you may recall, on a recent assignment, we wrote two procedures, (riffle2 lst1 lst2), a procedure that riffles two lists together, and (riffle3 lst1 lst2 lst3), a procedure that riffles three lists together.

For example,

> (riffle2 (list 'a 'b 'c) (list 'x 'y 'z))
'(a x b y c z)
> (riffle2 (list 'a 'b 'c) (range 10))
'(a 0 b 1 c 2 3 4 5 6 7 8 9)
> (riffle2 (range 10) (list 'a 'b 'c)
'(0 a 1 b 2 c 3 4 5 6 7 8 9)
> (riffle2 null (list 'a 'b 'c))
> (riffle3 (list 'a 'b 'c) (list 'p 'q 'r) (list 'x 'y 'z))
'(a p x b q y c r z)
> (riffle3 (list 'a 'b 'c 'd) (list 'e 'f) (list 'g 'h 'i))
'(a e g b f h c i d)
> (riffle3 (list 'a 'b 'c) null (list 'd 'e 'f))
'(a d b e c f)
> (riffle3 (list 'c 'b 'a) (list 'd 'd 'd) (list 'e 'f 'g))
'(c d e b d f a d g)

Here’s a concise definition of riffle2.

;;; Procedure:
;;;   riffle2
;;; Parameters:
;;;   lst1, a list
;;;   lst2, a list
;;; Purpose:
;;;   "Riffles" the two lists together.
;;; Produces:
;;;   riffled, a list
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   * Let len be the smaller of (length lst1), (length lst2)
;;;   * For all i, 0 <= i < len
;;;       * (list-ref riffled (* 2 i)) = (list-ref lst1 i)
;;;       * (list-ref riffled (+ 1 (* 2 i))) = (list-ref lst2 i)
;;;   * If (length lst1) > len
;;;     (drop riffled (* 2 len)) = (drop lst1 len)
;;;   * If (length lst2) > len
;;;     (drop riffled (* 2 len)) = (drop lst2 len)
(define riffle2
  (lambda (lst1 lst2)
    (if (null? lst1)
        lst2
        (cons (car lst1) (riffle2 lst2 (cdr lst1))))))

Here’s a similarly concise definition of riffle3.

;;; Procedure:
;;;   riffle3
;;; Parameters:
;;;   lst1, a list
;;;   lst2, a list
;;;   lst3, a list
;;; Purpose:
;;;   "Riffles" the three lists together.
;;; Produces:
;;;   riffled, a list
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   * Let len be the smaller of (length lst1), (length lst2), (length lst3)
;;;   * For all i, 0 <= i < len
;;;       * (list-ref riffled (* 3 i)) = (list-ref lst1 i)
;;;       * (list-ref riffled (+ 1 (* 2 i))) = (list-ref lst2 i)
;;;       * (list-ref riffled (+ 2 (* 2 i))) = (list-ref lst3 i)
;;;   * If (length lst1) = len
;;;     (drop riffled (* 3 len)) = (riffle2 (drop lst2 len) (drop lst3 len))
;;;   * If (length lst2) = len
;;;     (drop riffled (* 3 len)) = (riffle2 (drop lst1 len) (drop lst3 len))
;;;   * If (length lst3) = len
;;;     (drop riffled (* 3 len)) = (riffle2 (drop lst1 len) (drop lst2 len))
(define riffle3
  (lambda (lst1 lst2 lst3)
    (if (null? lst1)
        (riffle2 lst2 lst3)
        (cons (car lst1) (riffle3 lst2 lst3 (cdr lst1))))))

As you may have noted, the strategy for writing a concise version of riffle` is fairly straightforward:

  • When the first list is null, throw it away.
  • Grab the first element of the first list.
  • Recurse, moving the cdr of the first list to the end.

Write, but do not document, a recursive procedure, (riffle list-of-lists) that takes a list of lists as a parameter and riffles them together using similar policies.

> (riffle '((1 2 3)))
'(1 2 3)
> (riffle '(() (a b c)))
'(a b c)
> (riffle '((a b c) (x y z)))
'(a x b y c z)
> (riffle '((a b c) (1 2 3 4 5 6)))
'(a 1 b 2 c 3 4 5 6)
> (riffle '((1 2 3 4 5) (a b)))
'(1 a 2 b 3 4 5)
> (riffle '((a b c) (d e) (f) (g h) (i j k)))
'(a d f g i b e h j c k)
> (riffle '((a b c) (d e) (f) (g h) (i j k) (l m n o p)))
'(a d f g i l b e h j m c k n o p)
> (riffle '())
'()

Note: Two basic strategies come to mind. (1) You can use a generalized version of the strategy we just described. (2) You can probably do something interesting with filter and map (and some recursion).

Problem 4: Whatzitdo?

Topics: recursion over lists, named let, code reading, documentation, higher order procedures

Consider the following interesting procedure definition.

(define s (lambda (l) (lambda (p?) (let s ([a null] [b null] [c
p?]) (cond [(null? c) (list (reverse a) (reverse b))] [(l (car c))
(s (cons (car c) a) b (cdr c))] [else (s a (cons (car c) b) (cdr
c))])))))

Determine what this procedure does and then do the following.

a. Reformat the procedure so that it follows reasonable standards. (You need not show this version; we only want to see the version in part b.)

b. Rename the identifiers that they will make sense to the reader.

c. Write 6P-style documentation for the renamed procedure.

d. Explain why there are two calls to reverse in the base case.

Problem 5: Tallying vectors

Topics: vectors, vector recursion, higher-order procedures

Document and write a procedure, (vector-tally vec pred?), that counts the number of values in vec for which pred? holds.

> (vector-tally (vector 1 2 3 4 5) odd?)
3
> (vector-tally (vector 1 2 3 4 5) even?)
2
> (vector-tally (vector 1 2 3 "four" 'five) odd?)
Error! odd?: contract violation
Error!  expected: integer
Error!  given: "four"
> (vector-tally (vector 1 2 3 "four" 'five) (conjoin integer? odd?))
2

Note: You may not convert the vector to a list (e.g., with vector->list), nor may you generate lists along the way (e.g., using map).

Problem 6: Deep recursion, revisited

Topics: deep recursion, patterns of recursion, higher-order procedures

As you may recall, we wrote a definition of deep-reverse that looked like the following.

;;; Procedure:
;;;   deep-reverse
;;; Parameters:
;;;   val, a Scheme value
;;; Purpose:
;;;   Reverses the value (most frequently, a nested list).
;;; Produces:
;;;   reversed, a Scheme value
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   * If val is not a list
;;;     reversed is val
;;;   * If val is a list
;;;       * (length reversed) = (length val)
;;;       * For each i, 0 <= i < (length reversed)
;;;         (= (deep-reverse (list-ref reversed i))
;;;            (deep-reverse (list-ref val (- (length val) i 1))))
(define deep-reverse
  (lambda (val)
    (if (list? val)
        (reverse (map deep-reverse val))
        val)))
> (deep-reverse '())
'()
> (deep-reverse '(a b c))
'(c b a)
> (deep-reverse '(a (b (c d) e) (f (((g h) i)))))
'((((i (h g))) f) (e (d c) b) a)

Here’s a definition of deep-largest that uses a similar structure.

;;; Procedure:
;;;   deep-largest
;;; Parameters:
;;;   val, a Scheme value
;;; Purpose:
;;;   Finds the largest real number in val
;;; Produces:
;;;   largest, a real number
;;; Preconditions:
;;;   val is either a real number or a deep list of real numbers.
;;; Postconditions:
;;;   * largest is one of the values in val.
;;;   * If r is a value in val, (>= largest r).
(define deep-largest
  (lambda (val)
    (if (list? val)
        (apply max (map deep-largest val))
        val)))
> (deep-largest 4)
4
> (deep-largest '(4 3 2 1))
4
> (deep-largest '(1 2 3 4))
4
> (deep-largest '(-4 -10 -2))
-2
> (deep-largest '((1 2) 4 ((((100) 80) 11) 3) (5 -4)))
100
> (deep-largest '((1 2) 4 ((((80) 11) 3) (5 -4))))
80

Here’s a definition of deep-tally-odd that uses a similar, but slightly different, structure.

;;; Procedure:
;;;   deep-tally-odd
;;; Parameters:
;;;   val, a Scheme value
;;; Purpose:
;;;   Counts the number of odd values in val.
;;; Produces:
;;;   tally, an integer
;;; Preconditions:
;;;   val is either an integer or a deep list of integers
;;; Postconditions:
;;;   tally represents the number of odd integers that appear in val.
(define deep-tally-odd
  (lambda (val)
    (if (list? val)
        (apply + (map deep-tally-odd val))
        (if (odd? val) 1 0))))
> (deep-tally-odd '((1 2) 4 ((((80) 11) 3) (5 -4))))
4
> (deep-tally-odd '1)
1
> (deep-tally-odd '(1))
1
> (deep-tally-odd '())
0

Here’s yet another procedure that follows a similar pattern, this time to square all the values in a deep structure, leaving the shape of the structure the same.

;;; Procedure:
;;;   deep-square
;;; Parameters:
;;;   val, a Scheme value
;;; Purpose:
;;;   Squares all of the values in val.
;;; Produces:
;;;   squared, a Scheme value
;;; Preconditions:
;;;   val is either a number of a deep list of numbers
;;; Postconditions:
;;;   * squared has the same "shape" as val.  That is, if val
;;;     is a list, squared is a list of the same length, and
;;;     all of the sublists have the same shape.
;;;   * Any numbers in val appear as their squares in squared.
(define deep-square
  (lambda (val)
    (if (list? val)
        (map deep-square val)
        (sqr val))))
> (deep-square '((1) (2 (((3) 4) 5))))
'((1) (4 (((9) 16) 25)))
> (deep-square '())
'()
> (deep-square '(1 1/2 3.5))
'(1 1/4 12.25)
> (deep-square 5)
25

As you may recall, when we find that we have written similar procedures, we should write a procedure that generalizes the common aspects of those procedures, making any differences parameters to that procedure.

What is similar between all of these procedures? They all check if the parameter is a list and, if so, map the same procedure over that list and then do something with the result. They all do something with the parameter if it’s not a list, even if that “something” is “just leave the parameter as is”.

a. Document a procedure, deep-fun, that encapsulates the common aspects of deep-reverse, deep-largest, and deep-tally-odd

b. Write the deep-fun procedure.

c. Rewrite deep-reverse, deep-largest, and deep-tally-odd so that they use deep-fun to do most of the work.

d. Using deep-fun, write (deep-tally pred? val), a generalized version of deep-tally-odd that tallies the number of values for which the predicate holds.

Questions and Answers

We will post answers to questions of general interest here while the exam is in progress. Please check here before emailing questions!

General Questions

What is a general question?
A question that is about the exam in general, not a particular problem.
Are we allowed to talk to others about general issues from the first few weeks of class, such as the syntax of section?
No. We’ve had too many problems with “slippery slope” issues.
Questions should go to the instructors.
Do all the sections have the same exam?
Yes.
Can we still invoke the “There’s more to life” clause if we spend more than five hours on the exam?
Yes. However, we really do recommend that you stop at five hours unless you are very close to finishing. It’s not worth your time or stress to spend more effort on the exam. It is, however, worth your time to come talk to us, and perhaps to get a mentor or more help (not on this exam, but on the class). There’s likely some concept you’re missing, and we can help figure that out.
What do you mean by “implement?”
Write a procedure or procedures that accomplish the given task.
Do we have to make our code concise?
You should strive for readable and correct code. If you can make it concise, that’s a plus, but concision is secondary to readability and correctness. Long or muddled code is likely to lose points, even if it is correct.
Much of your sample 6P-style documentation has incomplete sentences. Can we follow that model? That is, can we use incomplete sentences in our 6P-style documentation?
Yes, you can use incomplete sentences in 6P-style documentation.
You tell us to start the exam early, but then you add corrections and questions and answers. Isn’t that contradictory? Aren’t we better off waiting until you’ve answered the questions and corrected any errors?
We think you’re better able to get your questions answered early if you start early. Later questions will generally receive a response of “See the notes on the exam.”
How do we know what our random number is?
You should have received instructions on how to generate your random number on the day the exam was distributed. If you don’t have a number, ask your professor for one before submitting your exam.
To show we’ve tested the code informally, would you just like us to just post the inputs we used to test the procedure? If so, how should we list those?
Copy and paste the interactions pane into the appropriate place in the definitions pane. Put a #| before the pasted material. Put a |# after the pasted material.
Should we cite our partner from a past lab or assignment if we use code from a past lab or assignment?
You should cite both yourself and your partner, although you should do so as anonymously as possible. For example “Ideas taken from the solution to problem 7 on assignment 2 written by student 641321 and partner.”
If we write a broken procedure and replace it later, should we keep the previous one?
Yes! This will help us give you partial credit if your final procedure isn’t quite right. But make sure to comment out the old one using #| and |# or semicolons. You might also add a note or two as to what you were trying to do.
Do I need to document helper procedures?
You must document helpers using the 4Ps.
Can I use procedures that we have not covered in class?
Probably not. Ask about individual procedures if you’re not sure.
I discovered a way to define procedures without using lambda that looks like (define (proc params) body). Can I use that?
No.
Is extra credit available on this exam?
No explicit extra credit is available. However, we may find that you’ve come up with such wonderful answers that we cannot help but give you extra credit. (Don’t laugh. It happens almost every time.)
DrRacket crashed and ate my exam. What do I do?
Send your instructor the exam file and any backups that DrRacket seems to have made. (Those tend to have a similar file name, but with pound signs or tildes added at the front or back.) In the future, email yourself a copy of the exam each time you pause.
How do I know when you’ve added a new question or answer?
We try to add a date stamp to the questions and answers. Those without a date stamp were likely present when the exam was released.
Should we cite readings from the CSC 151 Web site? If so, how much information should we give?
Yes. The title and URL should suffice.
Can I use other websites as a reference for the exam?
As the exam policies page states, “This examination is open book, open notes, open mind, open computer, open Web.” Note that it also states “If you find ideas in a book or on the Web, be sure to cite them appropriately.”
Do we have to cite the exam itself?
No.
How do I generate a random six digit number?
(random 1000000). If you end up with a number less than six digits, add zeros to the left side of the number until you have six digits.
Some of the problems are based on a recent homework assignment. Should I cite my work on that assignment?
If you refer to your work on that assignment in solving the problem, you should cite your assignment.
How do I cite my work on an assignment?
Something like “Student 123456 and partner. Homework Assignment 3. CSC 151.01. 6 February 2019.”
I see the comment ; STUB on some of the procedures. What does that mean?
We use the term “STUB” for a procedure that gives some default value rather than the correct one. Stub procedures are useful when you need the procedure to exist during development, but have not yet had time to implement it. You should remove these comments as you replace stub procedures with real implementations.
Are there any requirements for informal tests?
Sort of. They should be good tests, though they need not be exhaustive. Your informal tests only count if they are different from the examples on the exam.
Do we need to show examples of running our helper procedures?
No, but you should be checking them carefully. You don’t need to include these examples in your exam.
How much time do you think each problem should take?
You can keep the times lower by sending us questions when you hit barriers.
Some of the problems say “Write, but do not document”. Should we write 4P-style documentation for such procedures?
We’ve done our best to provide documentation for those procedures.
If I’ve written a found a higher-order procedure that returns a procedure, should I document both?
Yes.
What is the most common issue you’ve helped with?
A surprising number of people are calling their kernels with the parameters in the wrong order.

Questions on problem 1

Questions on problem 2

Is it okay if I return a fraction (a rational number) rather than something with decimals?
Yes.

Questions on problem 3

Questions on problem 4

Questions on problem 5

Questions on problem 6

Do I need to document the new procedures in 6c and 6d?
No. The ones in 6c are effectively documented already.
I can’t write this with only one parameter.
You shouldn’t. We were intentionally requiring you to figure out what parameters are appropriate. But we needed something, so we just put in val.
I don’t understand what you mean by “encapsulates the common aspects” and “rewrite…in terms of deep-fun”.
In the reading on Design patterns and higher-order procedures, we saw that once we found ourselves using a similar set of code for a variety of procedures (e.g., all-integer?, all-real?, etc.), we could factor out the common part and then rewrite the procedure in terms of that new procedure.
Once we’ve written all (as in the reading), we can write
(define all-integer?
(lambda (lst)
  (all integer? lst)))
That is, all encapsulates the common aspects and the new all-integer? is defined in terms of all.
Your goal is to write something like all, except for the deep recursion pattern exhibited in those four procedures. You should then rewrite the procedures in terms of deep-fun, just as we rewrote all-integer? in terms of all.
I don’t see how to express reverse, apply +, and apply max in
the same form.
All three are procedures.
How is apply + a procedure?
You can think of it as (lambda (lst) (apply + lst)) or as (section apply + <>).

Errata

Please check back periodically in case we find any new issues.

  • There is a mismatch between the documentation and the code for square-root. [NC, 1 point]
  • “assignment” was misspelled as “assingment” in problem 3. [JT, 1 point]
  • “riffle3” was misspelled as “riffl3” in problem 3. [JT, 1 point]
  • We used iota instead of range. [JM, 1 point]
  • “semesters’” had an extra “s”. [SM, 1 point, only Rebelsky seection]
  • Missing “not” in the instructions. [AB, 1 point]

Acknowledgements

Some of the problems on this exam are based on—and at times copied from—problems on previous exams for the course. Those exams were written by Charlie Curtsinger, Janet Davis, Fahmida Hamid, Rhys Price Jones, Titus Klinge, Samuel A. Rebelsky, John David Stone, Henry Walker, and Jerod Weinman. Many were written collaboratively, or were themselves based upon prior examinations, so precise credit is difficult, if not impossible.

Some problems on this exam were inspired by conversations with our students and by correct and incorrect student solutions on a variety of problems. We thank our students for that inspiration. Usually, a combination of questions or discussions inspired a problem, so it is difficult and inappropriate to credit individual students.