Fundamentals of Computer Science II (CSC-152 99F)


Notes on Assignment 4: Linked Lists

A Sample Solution

Although a number of you used some fairly nice ideas in your solutions, including an extra field to keep track of the end of the list, I've gone for a somewhat minimalist solution. I have taken advantage of Node.nil() to create my empty lists and Node.isEmpty() to check for empty lists.


import Node;
import CursoredList;
import Comparator;
import IncomparableException;

/**
 * An implementation of cursored lists using linked lists, with nodes
 * that connect to each other.  Written as part of the solution to
 * HW4 of CSC152 99F and also for exam 3 of the same course.
 *
 * Specific documentation for each method can be found in the CursoredList
 * interface.
 *
 * @author Samuel A. Rebelsky
 * @version 1.0 of November 1999
 */
public class CursoredLinkedList
  implements CursoredList
{

  // +--------+--------------------------------------------------
  // | Fields |
  // +--------+
  
  /**
   * The node that contains the first element of the list.  This is set to
   * null (or, more precisely, Node.nil()) when the list is empty.
   */
  protected Node front;
  
  /**
   * The node that contains the current element of the list.  When this
   * is null, the position is unknown.
   */
  protected Node cursor;
  
  // +--------------+--------------------------------------------
  // | Constructors |
  // +--------------+
  
  /**
   * Create a new empty list.
   */
  public CursoredLinkedList() {
    front = Node.nil();
    cursor = null;
  } // CursoredLinkedList()
  
  // +---------+-------------------------------------------------
  // | Methods |
  // +---------+
  
  /**
   * Add an element to the end of the list.
   */
  public void addToEnd(Object element) {
    // Special case: The list is empty.
    if (Node.isEmpty(front)) {
      // Create a new node for the front of the list.
      front = new Node(element, Node.nil());
    }
    // Normal case: The list is nonempty
    else {
      // Find the last element in the list.
      Node finder = front;
      while (!(Node.isEmpty(finder.getNext()))) {
        finder = finder.getNext();
      } // while
      // Add after that
      finder.setNext(new Node(element, Node.nil()));
    } // The list is nonempty
  } // addToEnd(Object)

  /**
   * Add an element to the front of the list.
   */
  public void addToFront(Object element) {
    // Special case: The list is empty.
    if (Node.isEmpty(front)) {
      front = new Node(element, Node.nil());
    }
    // Normal case: The list is nonempty.
    else {
      // (1) Create a new node for the front of the list.
      // (2) Set its next element to the old front.
      // (3) Update front to refer to the new node.
      front = new Node(element, front);
    }
  } // addToFront(Object)

  /**
   * Add an element after the current element.
   */
  public void addAfterCursor(Object element) {
    // Don't have to worry about the special case in which the
    // list is empty, since the precondition says "the position
    // of the cursor is known", and the cursor has no known
    // position in the empty list.

    // (1) Create a new node whose next node is cursor's next element.
    // (2) Make that node the cursor's new next element.
    cursor.setNext(new Node(element, cursor.getNext()));
  } // addAfterCursor(Object)
 
  /**
   * Delete the current element.
   */
  public void delete() {
    // Special case: The cursor is at the front of the list.
    if (cursor == front) {
      front = front.getNext();
      // The position of the cursor is unspecified.
      cursor = null;
    } // cursor at front of the list
    // General case: The cursor is not at the front of the list.
    else {
      // Find the node that precedes the cursor.
      // Note that the loop must terminate if the cursor is in the list, 
      // and the cursor being in the list is a precondition.
      Node finder = front;
      while (finder.getNext() != cursor) {
        finder = finder.getNext();
      }
      // Skip over the cursor'd element.
      finder.setNext(cursor.getNext());
      // Let Java clean up the deleted node.
      cursor = null;
    } // general case
  } // delete()

  /**
   * Move the cursor to the next element in the list equal
   * to the specified element.
   */
  public void find(Object findMe)
    throws Exception
  {
    // If we need the NEXT equal element, we need to start after
    // the current element.
    cursor = cursor.getNext();
    // Keep going until we run off the list or find a match
    while ( (cursor != null) && (!findMe.equals(cursor.getContents())) ) {
      cursor = cursor.getNext();
    }
    // Have we run off the end of the list?
    if (cursor == null) {
      throw new Exception("Cannot find another copy of '" + findMe + "'");
    }
  } // find(Object)

  /**
   * Move the cursor to the next element in the list equal
   * to the specified element, using compare for comparisons.
   */
  public void find(Object findMe, Comparator compare)
    throws Exception
  {
    // Keep going until we run off the list or find a match.
    // This doesn't match the code above because the equals
    // method can throw an exception.
    boolean done = false;
    while (!done) {
      // If we need the NEXT equal element, we need to start after
      // the current element.
      cursor = cursor.getNext();
      try {
        done = ( (cursor == null)
                 || (compare.equals(findMe, cursor.getContents())) );
      }
      catch (IncomparableException e) {
        // Do nothing.  If the two things weren't comparable, so
        // they can't be equal.  If we've already run off the end
        // of the list, cursor will be null, so we don't call the
        // exceptional code.
      }
    } // while
        
    // Have we run off the end of the list?
    if (cursor == null) {
      throw new Exception("Cannot find another copy of '" + findMe + "'");
    }
  } // find(Object, Comparator)

  /**
   * Move the cursor to the front of the list.
   */
  public void front() {
    cursor = front;
  } // front()

  /** 
   * Get the current element of the list.
   */
  public Object getCurrent() {
    return cursor.getContents();
  } // getCurrent()

  /**
   * Advance the cursor to the next elmeent.
   */
  public void advance()
    throws Exception
  {
    cursor = cursor.getNext();
    if (cursor == null) {
      throw new Exception("Ran off of the list");
    }
  } // advance()

} // class CursoredLinkedList


Some Common Problems

Special Cases

The general cases of most of the methods in the linked list implementation are relatively straightforward. However, many methods also have some special cases in which you have to be careful about effects on seemingly-independent parts of the implementation. Here are a few that some of you missed.

Some of you considered special cases, but then didn't document them (and sometimes ended up writing nearly-identical code for each case.

Using the Cursor

For an unclear reason, a number of you felt it necessary to move the cursor all over the place as you implemented various methods, including addToEnd. Unless you specifically document the fact that the cursor moves, you are not allowed to move the cursor. (Arguably, since the interface says nothing about moving the cursor, you really shouldn't move it for that method.)

Commenting

Most of your introductory comments were fine. However, I would have liked to have seen more in-code comments explaining what's going on. I often saw multiply-nested conditionals with no clear explanation as to why particular conditions were being tested.

Comparing Objects

A number of you used == to compare objects in find. However, as I mentioned in class, you really should use the equals methods, as in

  if (findMe.equals(cursor.getContents())) {
    // ...
  }

What's the difference? The equals method is ``has the same value'', the == is ``is the identical object''. Since find will generally be used for ``find something like this'', it makes more sense to use equals.

Exceptions

Many of you are not particularly careful in how you use exceptions. In most cases, it seemed to be more sloppiness than a lack of understanding. However, we may want to go over exceptions again.

Printing Error Messages

A small group of you decided to print error messages when some method you called threw an exception. However, you were writing a utility class, one that does not interact directly with the user. Hence, you should not print any messages for the user. The only contact you should have should be with the calling method, and that contact is done through return values and exceptions.

How would you feel if, whenever Netscape or Microsoft Word encountered a small internal error, it printed a message on your screen?

Useless Code

I was surprised to see a number of you write what I might call ``useless'' code. Commands that are ignored, serve no purpose, or are immediately obviated. Here are some examples.

Here's an interesting initialization.

    Node tmp = new Node(null,null);
    tmp = cursor;

This says,

    Create a new Node with no next node and null
    as its contents.  Create a new variable, tmp, and let it
    refer to that newly created node.  Then, let tmp refer to
    the same node that the cursor refers to.

You could write it much more efficiently as

    Node tmp = cursor;

Here's an interesting combination of conditionals and exceptions.

    try {
      if (Node.isEmpty(front)) { }
    }
    catch (Exception e) {
      System.out.println("The list is empty.");
    }

This says,

Determine if the front of the list represents an empty list.  If it
does, do nothing.  If it's not, do nothing.  If you happen to get
an exception (unlikely, since isEmpty doesn't throw
exceptions), print an error message that (1) you shouldn't be printing
and (2) doesn't necessarily correspond to the error.

You probably meant to write

    if (Node.isEmpty(front)) {
      throw new Exception("Cannot do X with an empty list.");
    }
    else {
      // ...
    }

Here's a small, but significant, problem of ordering.

    while ( (!findMe.equals(cursor.getValue())) && (cursor != null) ) 
      cursor = cursor.getNext();

The problem is that you ask for cursor.getValue() before you ensure that cursor is not null. You wouldn't catch this error because the method that includes it is supposed to throw an exception at just this time. However, it throws a different exception than you expect.

Finally, here's another interesting exception example.

    try {
      this.advance();
    }
    catch (Exception e) {
      throw e;
    }

This says,

Try to advance the list.  If that fails, inform the caller that it failed.

While that is reasonable, it is also overly verbose. You can just as easily write.

    this.advance();

Since it's not in a try/catch clause, Java knows to throw the exception to the caller of the current method.

Constructors

A few of you neglected to write constructors for your class. You should never take advantage of default assignments to fields; rather, you should make sure to initialize every field in your constructor.

Design Problems

As we've already discussed in class, the interface had some design problems. I will admit that while some were intentional, others were not. For example, it is difficult to determine the number of elements in the list. Some of the comments were also intentionally ambiguous.

History

Thursday, 18 November 1999


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.

This page may be found at http://www.math.grin.edu/~rebelsky/Courses/CS152/99F/Assignments/notes.04.html

Source text last modified Thu Nov 18 21:29:20 1999.

This page generated on Thu Nov 18 21:54:26 1999 by Siteweaver. Validate this page's HTML.

Contact our webmaster at rebelsky@grinnell.edu