This is the second midterm exam that was given to CS1321X students in
Fall 2002. This should give you an idea of the kinds of questions we
might be asking on Thursday. For a true testing experience, turn off
the TV, close the door, and give yourself 80 minutes to answer these
questions and see how you do.
CS 1321X
Quiz 2 Fall 2002
There are 100 points possible on this exam. You have 80 minutes to
complete it and turn it in. Put only what you want to be graded on
the front sides of the pages, and use the back sides of the pages as
scratch paper. If you run out of room on the front side of the page,
you can put part of your answer on the back of that same page, but
make sure you note on the front of the page that there's stuff to be
graded on the back. And please strive for legibility, because we
don't give credit for stuff we can't read. For this exam, stay within
the functional programming paradigm and use only recursive programming
techniques for your control structures. And in case you've forgotten,
the programming language we use here is Scheme.
1. (20 points): Using Scheme, create a function called remove-all
which takes two arguments, any list and an item to be removed from
that list, and returns the list with all occurrences of that item
removed from that list, no matter how deeply nested the item may be.
For example:
> (remove-all 'x '((a (x) b x) c x ((d x ((x))))))
((a () b) c ((d (()))))
> (remove-all '(x) '((a (x) b x) c x ((d x ((x))))))
((a b x) c x ((d x ())))
> (remove-all 'x '(a x b x c (x)))
(a b c ())
> (remove-all 'x '())
()
>
2. (10 points): Below is a slight variation on the Scheme preorder
tree traversal function that we discussed earlier in the semester:
(define (print-preorder tree)
(cond ((null? tree) ())
(else (write (car tree)) ; prints (car tree) on screen
(newline) ; moves to next line on screen
(print-preorder (cadr tree))
(print-preorder (caddr tree))))))
Also, here is yet another list representation of a simple binary tree
(the first element is the root, the second element is the left subtree,
and the third element is the right subtree):
(a (b (d () ()) (e () ())) (c (f () ()) (g () ())))
If we passed this list to the print-preorder function as a parameter,
we'd see the following result:
> (print-preorder '(a (b (d () ()) (e () ())) (c (f () ()) (g () ()))))
a
b
d
e
c
f
g
()
>
If you look carefully at the function, you'll see that nodes in the tree
are only printed if they're not (). Yet () clearly appears at the end of
the printed list of nodes when this function is evaluated. Why?
3. (20 points): The print-preorder function shown in Problem 2 doesn't
offer much in the way of data abstraction. A better way to write this
function would be as follows:
(define (print-preorder tree)
(cond ((null? tree) ())
(else (process-root tree)
(process-left-subtree tree)
(process-right-subtree tree))))
In the space below, show the definitions for process-root,
process-left-subtree, and process-right-subtree that would make this
new print-preorder function display the same thing on the screen as
the old print-preorder function did when passed the same tree. Define
only three functions...no more and no less.
4. (10 points): Now that data abstraction principles have been
suitably employed in the construction of the print-preorder function,
it should be easy to adapt the function to new representations of the
data. For example, here's a different list representation of the binary
tree:
(((() () d) (() () e) b) ((() () f) (() () g) c) a)
In this representation, a binary tree is represented by a three-element
list in which the first element represents the left subtree (which
itself may be another three-element list), the second element represents
the right subtree (which also may be a three-element list), and the
third element is an atom representing the root.
Of course, print-preorder as defined above will no longer print the correct
sequence of nodes if passed a tree represented in this new way. Without
changing the new main print-preorder function from Problem 3, rewrite
process-root, process-left-subtree, and process-right-subtree so that
print-preorder will print the correct sequence of nodes when it is
passed this new representation of the tree. Again, define only three
functions.
The figure below shows a high-level abstraction of a binary search tree.
55
/ \
/ \
/ \
25 70
/ \ / \
/ \ / \
12 30 62 80
/ \ / \ \
/ \ / \ \
6 15 60 68 99
\
\
7
If we take the abstraction down a level, an equivalent Scheme data
structure could look like this:
(55 (25 (12 (6 () (7 () ())) (15 () ())) (30 () ()))
(70 (62 (60 () ()) (68 () ())) (80 () (99 () ()))))
In converting our high-level abstraction to a Scheme structure, we
followed these conventions:
1. Every tree is represented by a list with three elements.
2. The first or root element must be a node (a number, in this case).
3. The second and third elements represent the descendants of the node
on the left and right, respectively. These elements can be either
another tree or (), indicating that there are no descendants on that side.
This information may be helpful in solving the next problem.
5. (20 points): Binary search trees have the nice property that
searching for something in one is relatively quick. A binary search
algorithm to find a given key (i.e., a number) in a tree might look
like this:
binary-search
if the tree is empty then return failure
if the value of the key is equal to the value at the root of the tree
then return success
if the value of the key is less than the value at the root of the tree
then perform binary-search on the left subtree
else perform binary-search on the right subtree
In the space below, use Scheme to construct a function called binary-search
that implements the algorithm given above. (Do not just flatten the list
and use member.) Your function should take two arguments. The first
argument is an integer representing a key to be found in a binary search
tree, and the second argument is the binary search tree itself, represented
in the list format shown on the previous page. Of course, you'll want to
incorporate all those nice things you've learned about functional
programming, modularity, abstraction, readability, and so on, in your
design. The binary-search function should work like this:
> (binary-search 12 '(55 (25 (12 (6 ... (99 () ()))))
#t
> (binary-search 73 '(55 (25 (12 (6 ... (99 () ()))))
#f
>
6. (20 points): There's a class of math (combinatorics) problems that are
easily solved using a divide-and-conquer approach which in turn can be
implemented in Scheme via tree or multiple recursion. One classic example
of this type of problem is called "The Towers of Hanoi."
Assume that you have three pegs and a set of disks, all of different
diameters, with holes in them (so that they can slide onto the pegs).
Start with all the disks on a single peg, in order of size (with the
smallest on top). The object of the puzzle is to move the pile of
disks to a specified peg, by moving one disk at a time. A legal move
consists of taking the top disk from any peg and putting it on either
of the other two pegs; but a disk may never be placed on top of a disk
that is smaller than itself.
You are to construct here a procedure move-tower that takes four
arguments---the number of disks in the pile, the peg the disks are on,
the peg the disks should be moved to, and the extra peg---and prints
the sequence of moves. For example, consider moving three disks from
peg1 to peg3 by evaluating
> (move-tower 3 1 3 2)
This should print:
move top disk from peg 1 to peg 3
move top disk from peg 1 to peg 2
move top disk from peg 3 to peg 2
move top disk from peg 1 to peg 3
move top disk from peg 2 to peg 1
move top disk from peg 2 to peg 3
move top disk from peg 1 to peg 3
If you try to solve this puzzle by thinking of individual moves in a
particular case (such as the one solved above), then you are not likely
to come up with a general solution (one that works for any number of disks).
You must find a way to think about the problem in general. There is a
powerful strategy (similar to mathematical induction) for thinking about
such problems, called "wishful thinking" (which is just a form of
abstraction). The idea is to
decide what would make the problem simpler;
pretend you already know how to solve the simpler version of
the problem:
figure out how to use the solution of the simpler problem to
construct a solution to the original problem
In the case of The Towers of Hanoi, it's pretty clear what would make the
problem simpler, namely having fewer disks. So let's assume that we know
how to solve the puzzle for any number of disks less than the number we've
been asked to move. We can then solve the puzzle in three steps:
move all but the bottom disk to the extra peg (by wishful
thinking), thus leaving the biggest disk behind
move the leftover (biggest) disk to the destination peg
move the pile of disks we stored on the extra peg to the
destination peg (which now has the biggest disk on it)
But we cannot always reduce the problem to a simpler one: There is nothing
easier than moving 0 disks. So if we are asked to move 0 disks, we'd better
not try to follow the above steps; rather, we should do nothing.
This plan can be directly translated into a procedure, which you are to do
in the space below (using Scheme, of course). We'll provide the necessary
print function:
(define (print-move from to)
(newline)
(display "move top disk from peg ")
(display from)
(display " to peg ")
(display to))
Your version of move-tower goes here:
Copyright (c) 2003 by Kurt Eiselt. All rights reserved, with
the exception of stuff that belongs to somebody else.
Last revised: October 20, 2003