Fundamentals of Computer Science II (CSC-152 98S)


Solutions to Examination 2

The exam was reformulated to be out of eighty (80) points, making one of the first three questions optional.

Problems

Each of the following problems has an assigned point value. While these values are partially influenced by the expected complexity or time requirements of each problem, there is no guarantee that higher points means harder or longer.

1. Improving the Merge in Merge Sort [20 points]

Herman and Helga Hacker recently learned about the legendary merge sort algorithm and have decided that the traditional merge subalgorithm is inefficient because it uses additional space. They decide to replace the traditional merge with the following algorithm (which appears as part of an ExtendedVector class). Critique their solution.

/**
 * Merge two contiguous ordered subvectors.
 * pre: 0 <= start <= mid < end < size
 * pre: the first subvector (start ... mid) is sorted.  That is,
 *   for all i, start <= i < mid, elementAt(i) <= elementAt(i+1)
 * pre: the second subvector (mid+1 ... end) is sorted.  That is,
 *   for all i, mid+1 <= i < end, elementAt(i) <= elementAt(i+1)
 * post: the whole subvector (start ... end) is sorted and contains
 *   the same elements as before (just in a different order).
 * running time: O(end-start)
 */
public void merge(int start, int mid, ind end) {
  int left=start;  // The "cursor" for the left subvector
  int right=mid+1; // The "cursor" for the right subvector
  while ((left <= mid) && (right <= end)) {
    // Make sure the smaller of the two current elements is
    // now in the left subvector.
    if (elementAt(right) < elementAt(left)) {
      swap(left,right);
    }
    // Increment the counters
    left = left + 1;
    right = right + 1;
  } // while
} // merge

There seems to be no clear rationale to their solution. While it makes sense to swap elements that are out of order, there is no guarantee that after the swapping the element swapped into the right subvector is in the right position. Consider the vector

3 4 1 5
L   R

with subvectors [3 4] (sorted) and [1 5] (also sorted). If start is 0, mid is 1, and end is 3, then when we swap the 3 and the 1, we are left with

1 4 3 5
  L   R

Now, the element at left is less than the element at right, so we're done (more or less). However, the resultant list is not sorted.

There are certainly other problems with the algorithm. For example, even if the strategy were generally correct, the Hackers make no attempt to deal with different size sublists (which will happen occasionally).

Can we improve their algorithm? Possibly. We might try moving the the left cursor and not the right cursor. This will have a problem with running time because we'll, in effect, insert the first element in the right list into the left list and then still be left with two lists that need to be merged. We could insert the elements from the second list one-by-one into the first list, but that's O(n^2) time. As far as I know, no one has figured out how to do in-place merging in O(n) time.

Note that some of you weren't satisfied with giving a non-working example and felt you had to say more. This was fine, until you went so far as to say something incorrect, in which case I felt it necessary to take off some points.

2. Biased Notation [20 points]

You may recall that the IEEE standard representation for single precision floating point numbers uses a biased notation for the exponent. In a biased notation, to represent the signed value N, you instead represent the unsigned value bias+N. For the following problems, you should use eight bits and a bias of 128.

What is the appropriate bias 128 representation of -17?

(-17) + 128 = 111 = 64 + 32 + 8 + 4 + 2 + 1

01101111

What is the appropriate bias 128 representation of 123?

123 + 128 = 251 = 128 + 64 + 32 + 16 + 8 + 2 + 1

11111011

What is the two's complement representation of -17?

Recall that in two's complement notation to negate a number you flip the bits and add 1.

17 = 16 + 1, which is represented as 00001001. Flip the bits, giving 11101110. Add 1, giving 11101111.

Hmmm ... that's the same as the bias-128 representation except that the leftmost bit is flipped.

What is the two's complement representation of 123?

123 = 64 + 32 + 16 + 8 + 2 + 1, which is represented as 01111011.

Hmmm ... once again it's the bias-128 representation with the leftmost bit flipped. Could I prove that?

What does 11010001 represent in bias 128 notation?

What does 01101101 represent in bias 128 notation?

What do you get if you add -5 and 7 (in bias 128 notation) using the standard addition strategy for unsigned binary numbers?

Represent -5: (-5) + 128 = 123 = 64 + 32 + 16 + 8 + 2 + 1, so 01111011.

Represent 5: 5 + 128 = 133 = 128 +4 + 1, so 10000101.

Represent 7: 7 + 128 = 135 = 128 + 4 + 2 + 1, so 10000111.

Represent -7: (-7) + 128 = 121 = 64 + 32 + 16 + 8 + 1, so 01111001.

     1111111
-5:  01111011
 7: +10000111
    ---------
    100000010

We drop the leftmost 1 (since we're limited to eight bits), giving 00000010.

Hmmm ... that's 2 in standard binary notation. However, we were working in excess-128, so we need to subtract 128, giving -126. Not very encouraging. However, we are off by only 128.

What do you get if you add 5 and 7?

         111
 5:  10000101
 7: +10000111
    ---------
    100001100

We drop the leftmost bit, giving 00001100. Hmmm ... that's 12, but we need to subtract 128, giving -116. Not good. Perhaps we need to add 128 to the result?

What do you get if you add 5 and -7?

           1
 5:  10000101
-7: +01111001
    ---------
     11111110

Yay! No overflow bit. However, the result is 254. When we subtract 128 we still get 126.

What do you get if you add -5 and -7?

     1111 11
-5:  01111011
-7: +01111001
    ---------
     11110100

Okay, that's 244. Subtract 128 we get 116.

What does this suggest?

That simply adding isn't enough. But wait! Every answer was off by 128 (-126+128=2; -116+128=12, 126-128=-2; 116-128=-12). Why is that? Basically, we were adding (X+128) + (Y+128) which gives (X+Y+128)+128, so we wouldn't expect to get the right number. Instead, we need to subtract 128 to get the right answer. However, because of the way the numbers are designed, adding 128 and subtracting 128 end up doing the same thing (flipping the left bit).

So, to add two numbers represented in excess-128, add them as you would any unsigned integers and then flip the leftmost bit.

3. Tree Traversal [20 points]

Our friends the Hackers are back again. This time, they've written a "generic" tree traversal algorithm. They claim "it can do both breadth-first and depth-first traversal of binary trees; all you have to do is select the right argument". With some prodding, they admit that their method only does preorder left-to-right traversals, but stick to their claim about "both breadth-first and depth-first". For once they may be right. Explain their claim.

/**
 * Print all the elements of a tree, using a linear structure
 * to help.
 */
public void printNodes (
  SimpleOutput out,
  BinaryNode root, 
  Linear collection) 
{
  // Just in case the collection has elements, reset it
  collection.reset();
  // Add the root.
  collection.add(root);
  // As long as the collection is not empty, remove an
  // element, print it out, then add its children.
  while (!collection.empty()) {
    Node next = collection.remove();
    out.println(next.toString() + " ");
    if (next.getLeft() != null) {
      collection.add(next.getLeft());
    }
    if (next.getRight() != null) {
      collection.add(next.getRight());
    }
  } // while the collection is not empty.
} // printNodes

This should seem very similar to our puzzle solving algorithm. If we use a stack as the linear structure, then we do a depth first search. If we use a queue as the linear structure, then we do a breadth-first search. Why? Using a stack, we complete the subtree for one child before we get to the next child (since we'll be pushing all the children of the first child before its sibling). Using a queue, we put the children of a node after its sibling.

Consider the following tree

    A
 B     C
D E   F G

If we pass in a queue, we get:

Observe that this is breadth-first, left-to-right, preorder.

If we pass in a stack, we get

Hmmm ... not quite left-to-right, but certainly depth-first and preorder. So, the hackers aren't quite as talented as I suggested.

A number of you made assumptions about the addition and removal policy for the Linear structure and forgot that both stacks and queues are linear structures. This made it difficult to get this question right.

4. List Deletion [40 points]

Believe it or not, our friends the Hackers have written a relatively elegant implementation of doubly linked lists. Unfortunately, they've forgotten to include a remove method and have asked you to write one. Your goal is to write and document a routine that removes all copies of an element from a doubly-linked list.

They've told you that they've used rebelsky.util.BinaryNode for their nodes and that the attributes of their class are as follows:

public class TheGreatestDoublyLinkedListEver {
  /**
   * The first element in the list.
   */
  protected BinaryNode first;
  /**
   * The last element in the list.
   */
  protected BinaryNode last;
  /**
   * The current element in the list.
   */
  protected BinaryNode current;
  /**
   * The size of the list.
   */
  protected int size;
  ...
} // TheGreatestDoublyLinkedListEver

Write and document the remove method that removes all copies of an element from the list. Make sure your documentation is at least as thorough as my typical documentation. Also make sure to indicate the running time of your algorithm.

/**
 * Remove all copies of an element, elt, from the current list.
 * Precondition: (none)
 * Postcondition: the list no longer contains any values 
 *    equal to of elt.
 * Postcondition: if the cursor was equal to elt, then 
 *   the cursor refers to the start of the list (unless the
 *   list is now empty).
 * Postcondition: any elements not equal to elt are still in 
 *   the list and still in the same order.
 *
 * @return The number of elements deleted.
 */
public int remove(Object elt) {
  // The number of elements we've removed
  int removed = 0;
  // The element we're currently looking at
  BinaryNode counter = front;
  // The next and previous elements (not strictly necessary,
  // but they do make it a little easier to read).
  BinaryNode prev, next;

  // Step through each element until we reach the end.  If we
  // identify an equal element, then we delete the element
  // by changing the previous reference of the next node and the
  // next reference of the previous node.
  while (counter != null) {
    // Should we delete the element?
    if (counter.getContents().equal(elt)) {
      next = counter.getNext();
      prev = counter.getPrev();
      // Is there a previous element?  If so, update it.
      if (prev != null) { prev.setNext(next); }
      // Is there a subsequent element?  If so, update it.
      if (next != null) { next.setPrev(prev); }
      // Have we affected the front, back, or current 
      // element of the list?  Note that we use "==" because
      // we actually want "points to the same node".
      if (counter == front) { front = next; }
      if (counter == back) { back = prev; }
      if (counter == current) { current = front; }
      // Update the size and number of elements we've removed
      removed = removed + 1;
      size = size - 1;
    } // if we should delete the element
    // Move on to the next element
    counter = counter.getNext();
  } // while

  // Done.  Return the number of elements we've removed.
  return removed;
} // Remove(Object)

Does this work with an empty list? Certainly. The body of the while loop is never executed. Should we restrict the method to nonempty lists? Probably not, but I didn't penalize those of you who did (unless you then failed to handle empty lists appropriately).

Does this work if it clears out the list? Well, it suffices to check the single element list (since longer lists will be reduced to the single elelment list). At that point, current, counter, front, and back will all point to the element that's equal and prev and next. will be null. So, when we update all of them, everything becomes null, which is how we represent the empty list.

What is the running time? It's an O(n) algorithm because we need to step through each element of the list. Can we do better? Probably not.

What were some common mistakes? [Listed so that those of you who didn't make them don't make them next time, and so those of you who made some see that others made similar and different errors.]

Extra Credit

Each of the following questions is worth two points if answered well (and zero if answered poorly).

How does Bob Cadmus's lecture on electronic imaging technology relate to what we've learned in CS152?

Good answers included:

List three things that Vannevar Bush should be famous for.


Disclaimer Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

Source text last modified Tue Dec 29 09:13:41 1998.

This page generated on Tue Jan 12 11:47:39 1999 by SiteWeaver.

Contact our webmaster at rebelsky@math.grin.edu