Algorithms and OOD (CSC 207 2013F) : Assignments

Exam 2: More Object-Oriented Design, ADTs, and Algorithms


This exam is now released. The repository should be available. The unit tests are are also now available.

Assigned: Tuesday, 19 November 2013

Prologue due: 5:00 p.m., Friday, 22 November 2013. Electronic version due: 10:30 p.m., Tuesday, 26 November 2013. Printed version due: 10:00 a.m., Wednesday, 27 November 2013.

Preliminaries

Exam Format

This is a take-home examination. You may use any time or times you deem appropriate to complete the exam, provided you return it to me by the due date.

This examination has a required prologue that must be completed by Friday afternoon. The prologue is intended to help you think about the examination.

There are four problems on this examination. You must do your best to answer all of them. The problems are not necessarily of equal difficulty. Problems may include subproblems. If you complete four problems correctly or mostly correctly, you will earn an A. If you complete three problems correctly or mostly correctly, you will earn a B. If you complete two problems correctly or mostly correctly, you will earn a C. If you complete one problem correctly or mostly correctly, you will earn a D. If you complete fewer than one problem correctly or mostly correctly, you will earn an F. If you do not attempt the examination, you will earn a 0. Partially correct solutions may or may not earn you a partial grade, depending on the discretion of the grader.

Read the entire examination before you begin.

I expect that someone who has mastered the material and works at a moderate rate should have little trouble completing the exam in a reasonable amount of time. In particular, this exam is likely to take you about eight hours, depending on how well you've learned the topics and how fast you work.

Academic Honesty

This examination is open book, open notes, open mind, open computer, and open Web. However, it is closed person. That means you should not talk to other people about the exam. Other than as restricted by that limitation, you should feel free to use all reasonable resources available to you.

As always, you are expected to turn in your own work. If you find ideas in a book or on the Web, be sure to cite them appropriately. If you use code that you wrote for a previous lab or homework, cite that lab or homework and the other members of your group. If you use code that you found on the course Web site, be sure to cite that code. You need not cite the code provided in the body of the examination.

Although you may use the Web for this exam, you may not post your answers to this examination on the Web. (You certainly should not post them to GitHub.) And, in case it's not clear, you may not ask others (in person, via email, via IM, via IRC, by posting a “please help” message or question on StackOverflow or on any other site, or in any other way) to put answers on the Web.

Because different students may be taking the exam at different times, and because some students will choose to take a makeup examination that involves redoing the problems, you are not permitted to discuss the exam with anyone until after I have indicated that it is acceptable to do so. If you must say something about the exam, you are allowed to say “This is among the hardest exams I have ever taken. If you don't start it early, you will have no chance of finishing the exam.” You may also summarize these policies. You may not tell other students which problems you've finished. You may not tell other students how long you've spent on the exam.

You must include both of the following statements on the cover sheet of the examination.

  1. I have neither received nor given inappropriate assistance on this examination.
  2. I am not aware of any other students who have given or received inappropriate assistance on this examination.

Please sign and date each statement separately. Note that the statements must be true; if you are unable to sign either statement, please talk to me at your earliest convenience. You need not reveal the particulars of the dishonesty, simply that it happened. Note also that inappropriate assistance is assistance from (or to) anyone other than Professor Rebelsky (that's me).

Presenting Your Work

You must present your exam to me in two forms, physically and electronically. That is, you must write all of your answers using the computer, print them out, number the pages, put your name on the top of every page, and hand me the printed copy. You must also submit an electronic copy of your exam.

I care that you are careful in your presentation on the printed copy. If you fail to name and number the printed pages, you may suffer a penalty. If you fail to turn in both versions, you may suffer a much worse penalty. If you fail to turn in a legible version of the exam, you are also likely to suffer some sort of penalty. Note that lines that wrap detract from legibility.

You should create the electronic version by making a tarball of any relevant code and emailing me the tarball. (If you don't know how to make a tarball, let me know.) If you really don't want to make a tarball, you can make a zip file. In either case, please make sure that the unpacked folder has four subfolders, one for each problem. Unless specified otherwise in the problem, the .java files for each problem should be in a src subfolder of the problem folder, and should not have a package declaration. You can also include .txt or .md files in each folder that have notes you would like me to read, answers to non-coding questions, and sample output from any experiments you conduct. You may include a .txt or .md file in the top-level directory with general notes.

In many problems, I ask you to write code. Unless I specify otherwise in a problem, you should write working code and include examples that show that you've tested the code. You should do your best to format that code to the Sun/Oracle Java coding standards.

You should document classes, interfaces, fields, and methods using Javadoc-style comments. You should specify preconditions and postconditions for each method.

Just as you should be careful and precise when you write code and documentation, so should you be careful and precise when you write prose. Please check your spelling and grammar. Since I should be equally careful, the whole class will receive one point of extra credit for each error in spelling or grammar you identify on this exam. I will limit that form of extra credit to five points.

I may give partial credit for partially correct answers. I am best able to give such partial credit if you include a clear set of work that shows how you derived your answer. You ensure the best possible grade for yourself by clearly indicating what part of your answer is work and what part is your final answer.

Getting Help

I may not be available at the time you take the exam. If you feel that a question is badly worded or impossible to answer, note the issue you have observed and attempt to reword the question in such a way that it is answerable. You should also feel free to send me electronic mail at any time of day.

I will also reserve time at the start of each class before the exam is due to discuss any general questions you have on the exam.

Preparation

Clone the repo using the following command, which will help ensure that you have the correct directory structure. Note that I've tried to set things up so that you can treat each problem as a separate Eclipse project.

$ git clone https://github.com/Grinnell-CSC207/exam2-2013F *username*

You will see that each projects includes a file called SamTest.java. I will put unit tests in those file before 5 p.m. Friday. (And I may continue to extend the unit tests in those files after I've put them in.) You can use git pull to grab those when they are available. If you write your own tests, please put them in a separate file so that we don't have merge conflicts.

Problems

Problem 1: Iterating Linked Lists

Topics: Linked structures, Anonymous inner classes, Iterators

In the code that accompanies this lab, you will find a simple implementation of circularly linked lists with a separate iterator class. In that separate class, we spend a lot of effort keeping track of things from the base class.

a. Annie and Mustafa believe that the code would be much cleaner if we just used an anonymous inner class. So rewrite the code to do so. Make sure that you refer appropriately to the fields of the enclosing class, rather than storing copies of those fields.

b. Remy and Delbert are disappointed that we never include remove (or is it delete?) in our iterators. Make them happy by implementing it.

Problem 2: Modeling Functions

Topics: Functions, Object-oriented design, Generics (parameterized types), Anonymous inner classes, Polymorphism.

Your classmates, Polly and Morpheus, really enjoyed the predicate problem from exam 1. (And no, I don't know why someone would name their child after a god of dreams. Perhaps they are fans of The Matrix or of Neil Gaiman.) And so they've decided we should go further and model general functions.

They're starting simple, just unary and binary functions. They want to take advantage of Java's static type checking, so they've used parameterized polymorphism (generics) in their design of such functions.

/**
 * A simple model of unary functions as objects.
 */
public interface UnaryFunction<Input, Output> {
    /**
     * Apply the function to a value.
     */
    public Output apply(Input arg) throws Exception;
} // interface UnaryFunction<Input,Output>

/**
 * A simple model of binary functions as objects.
 */
public interface BinaryFunction<LeftInput,RightInput,Output> {
    /**
     * Apply the function to a value.
     */
    public Output apply(LeftInput left, RightInput right) throws Exception;
} // interface BinaryFunction<LeftInput,RightInput,Output>

They've even written a few sample function objects, and even a function that builds function objects.

/**
 * Some functions on integers.
 */
public class IntegerFunctions {
    // +------------------+------------------------------------------------
    // | Function Objects |
    // +------------------+

    /**
     * Square an integer.
     */
    public static final UnaryFunction<Integer, Integer> square = new UnaryFunction<Integer, Integer>() {
	public Integer apply(Integer arg) {
	    return arg * arg;
	} // apply(Integer)
    }; // new UnaryFunction

    /**
     * Multiply two integers.
     */
    public static final BinaryFunction<Integer, Integer, Integer> multiply = new BinaryFunction<Integer, Integer, Integer>() {
	public Integer apply(Integer left, Integer right) {
	    return left * right;
	} // apply(Integer, Integer)
    }; // new BinaryFunction

    /**
     * Subtract one integer from another.
     */
    public static final BinaryFunction<Integer, Integer, Integer> subtract = new BinaryFunction<Integer, Integer, Integer>() {
	public Integer apply(Integer left, Integer right) {
	    return left - right;
	} // apply(Integer, Integer)
    }; // new BinaryFunction

    /**
     * Convert to a string.
     */
    public static final UnaryFunction<Integer, String> toString = new UnaryFunction<Integer, String>() {
	public String apply(Integer val) {
	    return val.toString();
	} // apply(Integer)
    }; // new UnaryFunction

    // +----------------+--------------------------------------------------
    // | Static Methods |
    // +----------------+

    /**
     * Create a function that adds x to its argument.
     */
    public static final UnaryFunction<Integer, Integer> adder(final int x) {
	return new UnaryFunction<Integer, Integer>() {
	    public Integer apply(Integer arg) {
		return arg + x;
	    } // apply(Integer)
	}; // new UnaryFunction
    } // adder(int)

} // IntegerFunctions

Now they're wondering what to do next. Inspired by Hoppy, a pet that their classmates have somehow smuggled in to the dorms, they decide that they should include a few of the standard higher-order procedures - compose, left-section, right-section, and map. They've even sketched the code.

/**
 * Some utilities for working with functions that are not yet implemented.
 */
public class FunctionUtils {
    /**
     * Given unary functions f and g, build the unary function f o g.  That
     * is, build the function (lambda (x) (f (g x))).
     * ...
     */
    public static <...> UnaryFunction<...> compose(final UnaryFunction<...> f,
            final UnaryFunction<...> g) {
        return new UnaryFunction<...>() {
            public ... apply(... arg) throws Exception {
                ...;
            } // apply
        }; // new UnaryFunction
    } // compose(UnaryFunction, UnaryFunction)
    
    /**
     * Given unary function f and binary function g, build the binary function 
     * f o g.  That is, build the function (lambda (x y) (f (g x y))).
     * ...
     */
    public static <...> BinaryFunction<...> compose(final UnaryFunction<...> f,
            final BinaryFunction<...> g) {
        return new BinaryFunction<...>() {
            public ... apply(... left, ... right) throws Exception {
                ...;
            } // apply
        }; // new BinaryFunction
    } // compose(Function)
    
    /**
     * Given a binary function, f, and two unary functions, g and h, build the
     * function (lambda (x,y) (f (g x) (h y)))
     * ...
     */
    public static <...> BinaryFunction<...> compose(final BinaryFunction<...> f,
            final UnaryFunction<...> g, final UnaryFunction<...> h) {
        return new BinaryFunction<...>() {
            public ... apply(... left, ... right) throws Exception {
                ...;
            } // apply
        }; // new BinaryFunction
    } // compose(Function)
    
    /**
     * Build a new function by filling in the left argument of f.
     */
    public static <...> UnaryFunction<...> leftSection(
            final BinaryFunction<...> f, final ... left) {
        return new UnaryFunction<...>() {
            public ... apply(... arg) throws Exception {
                ...;
            } // apply
        }; // new UnaryFunction
    } // leftSection
    
    /**
     * Build a new function by filling in the right argument of f.
     */
    public static <...> UnaryFunction<...> rightSection(
            final BinaryFunction<...> f, final ... right) {
        return new UnaryFunction<...>() {
            public ... apply(... arg) throws Exception {
                ...;
            } // apply
        }; // new UnaryFunction
    } // rightSection
 
    /**
     * Map a unary function over an array.
     *
     * @post
     *   For each i, 0 <= i < vals.length
     *     results[i] = fun.apply(vals[i])
     */
    public static <...> ...[] map(UnaryFunction<...> fun, ...[] vals) throws Exception {
         ...[] results = (...[]) new Object[vals.length];
         ...;
         return result;
    } // map

    /**
     * Map a binary function over two arrays.
     *
     * @pre
     *   left.length == right.length
     * @post
     *   For each i, 0 <= i < vals.length
     *     results[i] = fun.apply(left[i], right[i])
     */
    public static <...> ...[] map(BinaryFunction<....> fun, ...[] left,
            ...[] right) throws Exception {
        ....;
    } // map
} // class FunctionUtils

They use a lot of ellipses, don't they? That's because, as much as they like this idea, they're not quite sure how to achieve their goal. My first inclination was to have you do it for them. But Carl and Carla Clueless accidentally posted a solution, so that's difficult to ask you do to.

a. Spend approximately thirty minutes attempting to solve the problem.

b. Correct your solution after comparing it to their solution, perhaps using better typenames than they do.

c. You'll note that I've written some experiments to demonstrate the use of these various objects. Write four more interesting examples of the use of functions as objects. Put those in the file Examples.java.

Problem 3: Deletion in Binary Search Trees

Topics: Linked structures, Trees

We've started to explore the design of binary search trees. For convenience, we decided to make binary search trees “add only” structures. Right now, you can't remove a value from a BST.

As you might expect, that decision has frustrated your colleagues Remy and Delbert. They think that it's reasonable to want to get rid of items.

Their sensible colleagues suggest that there's a simple technique for doing deletion - you can simply put a null in for the value, and, when the value is null, insert throws an exception. Unfortunately, Remy and Delbert are extremists, and say “Simulated deletion is not deletion. Your approach clogs the tree with pointless nodes. And if the tree is clogged with pointless nodes, we can't guarantee O(logn) insertion and deletion.” Of course, they ignore the fact that we've yet to figure out how to keep the trees balanced, and that we probably won't learn how to do so until we take an upper-division course in algorithms and data structures (or until we search on Wikipedia). But, hey, they're annoying enough that we're going to listen to them.

Fortunately, they suggest a reasonably straightforward approach for removing values.

To delete the node containing key
If the node has no left subtree, 
  Replace the node by its right subtree.
Otherwise, if the node has no right subtree,
  Replace the node by its left subtree.
Otherwise, the node has two subtrees
  Shuffle the tree so that the node with the largest key in the
    left subtree replaces the node.  

Why does this work? We know that the largest key in the left subtree is larger than every other key in the left subtree. And, since it's in the left subtree, we know that it's smaller than every key in the right subtree. Hence, it can be at the root after we delete the old root.

Implement their algorithm.

Asa, Sam, Cam, Ida, and Ina worry a bit about that “Shuffle” instruction. And they also want to design some interesting unit tests. They draw a bunch of trees to help themselves understand what should happen in each situation. In drawing these trees, they use lowercase letters to represent nodes, and uppercase letters to represent tree-shaped groups of nodes. And their pictures all illustrate deletion at the root.

Original After delete/shuffle
01 - no right subtree
   m   
  / \
 /
G
   G   
02 - no left subtree
   m
  / \
     \
      N
   N   
03 - both subtrees, left subtree has no right subtree
      m
     / \
    /   \
   g     N
  / \
 /
D
      g
     / \
    /   \
   D     N
04 - both subtrees, left subtree has right subtree with no right subtree
      m
     / \
    /   \
   g     N
  / \
 /   \
D     j
     / \
    /
   I
      j
     / \
    /   \
   g     N
  / \
 /   \
D     I
05 - both subtrees, left subtree has right subtree with right subtree (l is the rightmost key in the left subtree, and has no left subtree)
      m
     / \
    /   \
   g     N
  / \
 /   \
D     J
     / \
    /   \
   I     l
        / \
      l
     / \
    /   \
   g     N
  / \
 /   \
D     J
     / \
    /  
   I  
06 - both subtrees, left subtree has right subtree with right subtree (l is the rightmost key in the left subtree, but has a left subtree)
      m
     / \
    /   \
   g     N
  / \
 /   \
D     J
     / \
    /   \
   I     l
        / \
       /
      K
      l
     / \
    /   \
   g     N
  / \
 /   \
D     J
     / \
    /   \
   I     K

They note that they aren't sure that there's a real implementation difference between the last four cases, but they thought it was easiest to think about them separately.

When they show their examples to Remy and Delbert, Remy and Delbert note that they would find it useful to find the parent of the rightmost node. But they don't say why.

Problem 4: Dutch Quicksort

Topics: Sorting, Quicksort, the Dutch National Flag Algorithm.

Quinn and Sol are a bit frustrated by our current implementation of Quicksort. They say “Quicksort seems to do a lot of extra work when the pivot appears multiple times in the array. Why can't we just group all of the elements equal to the pivot together, and only recurse on the values that are strictly smaller and strictly larger?

Duff, Nat, and Fran say “That seems to be a task for the Dutch National Flag algorithm, which can partition an array into three sections: red, white, and blue, or smaller, equal, and larger.

Write a version of Quicksort that uses this approach. That is, it should partition the array into three parts and only recurse on the left and right subparts.

Note: You may find it difficult to make the partition process a separate method, since, in addition to rearranging the items in the subarray, it now has to return two values: the lower bound and upper bound of the middle part of the subarray. here are three options for dealing with that issue: (1) You can move the partitioning process into the middle of Quicksort. (2) You can write a helper procedure that, given the start of the middle subarray, finds the end. (3) You can write a helper class that holds two integers and return an object in that class.

Extra Credit

You can earn a small amount of extra credit if you report on three interesting techniques or ideas you discover from reading my unit tests.

Include a file at the top level of your directory structure with your comments on those techniques or ideas.

Questions and Answers

Problem 1

How long did this problem take you?

Ten minutes. About five to reorganize the code. About five to implement the remove method. But I also understood my design pretty well, and had tried to design the iterator to make it easier to remove things. (Because I knew the design, I was also able to fix the errors revealed by unit testing really quickly.)

Can we just have one iterator, or must we support multiple iterators?

You must support multiple iterators. But if you create your anonymous inner class correctly, that will happen more-or-less automatically.
It's certainly fine if the other iterators that are in midstream stop working correctly after an element is removed using one of the iterators. But the iterator with the associated remove needs to keep working.

I note that you are doing something a bit strange for the iterator. Rather than staying one node back, as we've discussed in the past, you seem to be staying two nodes back. Am I write in assuming that you want to make sure that you have access to the previous node so that you can more easily delete?

You are correct that I keep a bit back so that removal is easier. You don't need to use this strategy. But I found that it works well.

I'm finding it difficult with conceptualizing “two back”. Can you give us an alrenate implementation?

Sure.
/**
 * Alternate iterators for linked lists.
 */
class AltIteratorLL<T> implements Iterator<T> {
    // +--------+----------------------------------------------------------
    // | Fields |
    // +--------+

    /**
     * We may need to access some of the internals of the linked list class.
     */
    LinkedList<T> list;

    /**
     * The iterator remains one element back from the node so that we can
     * easily remove it.
     */
    NodeLL<T> here;
    
    /**
     * When we start up, we're immediately before a node that we have not
     * yet iterated.  We use the boolean to keep track of such situations.
     */
    boolean visitedNext;
    
    // +--------------+----------------------------------------------------
    // | Constructors |
    // +--------------+

    public AltIteratorLL(LinkedList<T> list) {
        this.here = list.front;
        this.list = list;
        // We have not yet looked at the next node.
        this.visitedNext = false;
    } // AltIteratorLL

    // +---------+---------------------------------------------------------
    // | Methods |
    // +---------+

    public T next() {
        if (!this.hasNext()) {
            throw new NoSuchElementException();
        } // if we've reached the end
        // Advance if appropriate
        if (this.visitedNext) {
            this.here = this.here.next;
        } // If we've already visited the next node
        // Note that we've visited the next node (b/c we're about
        // to return its value. 
        this.visitedNext = true;
        // And we're done    
        return this.here.next.value;
    } // next

    public boolean hasNext() {
        if (this.here.next == this.list.front) {
            return false;
        } else if (this.visitedNext && (this.here.next.next == this.list.front)) {
            return false;
        } else {
            return true;
        } // if/else
    } // hasNext

    public void remove() throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    } // remove

} // AltIteratorLL<T>

Problem 2

How long did this problem take you?

20 minutes for the types and bodies. A lot of time for experiments, which I've now added to the repo. You may want to add your own.

Problem 3

How long did this problem take you?

Ten minutes. But I'd spent a lot of time thinking about it, which is why you have the pretty ASCII trees in the exam.

Why do we care about the parent of the largest element in the left subtree?

Because in order to move (“shuffle”) that element, we need to change the link from parent to child.

If you care about the parent so much, why don't you have a parent link?

Too much work. In addition, since we traverse the tree top to bottom, we should always pass by the parent before seeing a node. So, if you find you need the parent of a node, either (a) stop a level before reaching the node or (b) pass the parent along as a parameter. (I'd prefer that you use the first technique.)

Problem 4

How long did this problem take you?

Fifteen to twenty minutes. About five to write the invariant. About ten to write the loop and recursive calls. A minute or two to fix the one bug (I was getting the comparison backwards.) Having recently written DNF (early in the semester) and Quicksort helped a lot.

Should I write an invariant for the partition aspect of qsort?

I found that writing a visual invariant made it much easier to solve. So, I'd recommend that you write such an invariant. But i won't require it.

Did you write a separate partition method?

No. I found it faster and easier to put the partition code in the middle of the qsort method.

How did you identify the pivot?

I used the technique that Lea said they used in 301 - I used the value in the middle of the subarray as the pivot.
// Put the pivot at the front. Use CLRS "middle element" idea to
// identify the pivot.
swap(values, lb, lb + (ub-lb)/2);
T pivot = values[lb];

Miscellaneous

Some of the typos in your exam are very strange. It's like parts of the things you wrote disappeared.

I blame gremlins.

Errata

Here you will find errors of spelling, grammar, and design that students have noted. Remember, each error found corresponds to a point of extra credit for everyone. I usually limit such extra credit to five points. However, if I make an astoundingly large number of errors, then I will provide more extra credit.

I will not accept corrections for credit until after I have taken the examination out of draft mode.

I will not credit corrections for text in the Q&A and errata sections, since those are often written quickly to get information out to students. I may not credit corrections to grammatical mistakes in code.

  • Incomplete sentence: “And they really like to delete items, so they won't ...”. That sentence is now deleted. [EM, JB, AK, 1 point]
  • Incorrect verb form “insert thrown an exception”, should be “throws”. [JB, 1 point]
  • Missing “If you fail” in “If you fail to name and number the pages”. [AK, 1 point]
  • Extra d in “credit”. [MH, 1 point]
  • Missing question mark in parenthetical question. [FB, 1 point]
  • Improper computation of midpoint. [EW, 2 points]

Copyright (c) 2013 Samuel A. Rebelsky.

Creative Commons License

This work is licensed under a Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.