Return to lecture notes index
February 12, 2003 (Lecture 12)

Introduction

Last class we discussed linked lists. Today we are going to learn how to build them. We're going to approach this very slowly. Today, we are just going to look at the overall class structure and the simplest behaviors: adding to the list and looking through the list. Next class, we are going to look at a couple of more behaviors including removing items from the beginning and end of the list. We'll spend the rest of next class and a few more classes exploring data structures that can be constructed using simple lists, including stacks and queues, and some of the problems that they can be used to solve. Next, we'll come back to linked lists and explore far more complex behaviors and the problems that much richer lists can address.

Overview of LinkedList and Node

During our discussion of linked lists, we spoke of two different classes: LinkedList and Node. The Nodes were the containers, which maintained the position of an item by storing a reference to the item and a reference to its successor. The LinkedList was the user-visible data structure that contained the Nodes by storing references, head, tail and, possibly, index to them.

In building a linked list according to this model, we are going to make Node a private inner-class of LinkedList. This will make it inaccessible to methods outside of the LinkedList. Keep in mind that the Node is nothing that the user will ever see or know about. The user of the LinkedList will ask the LinkedList to add, delete, remove, and find items. The fact that the LinkedList is organized using Nodes is completely invisible to the user. In fact, many different container-like data structures might implement the same interface, but using different organizations.

The actual Java LinkedList does not place the Node into the LinkedList as an inner class. Instead, it encapsulated both into a structure known as a package. The LinkedList is labeled as a public class within the package and the Node is constructed as a private class within the package. This accomplishes the same thing, with slightly more protection. But, we're not going to get too deeply into packages in 15-111, so we'll do it with an inner class for now.

The big difference is that, since the Node is within the LinkedList, the LinkedList is able to access even the private parts of the Node. This is not the case for private classes within the same package.

Nesting the Classes

Notice how we've got the definition of the Node class nested within the LinkedList class.

  public class LinkedList {

    private class Node {
      // Notice the private access specifier in the declaration above
      // The fields and methods of the Node are defined here
    }

    // The fields and methods of the LinkedList are defined here

  }
  

Implementing the Node

What follows below is the definition of the Node class that we developed in class. Remember that this is within the LinkedList class.

  private class Node {
    private Object data; // The Object organized by this node
    private Object next; // A reference to the next Node in the list

    // When we create a new Node, we'll usually know where it is
    // being placed within the list. Since we'll always know
    // the corresponding user-Object, we want a constructor with both. 
    public Node (Object data, Node next)
    {
      // Notice the use of "this" to shift the scope from the 
      // parameters to the instance variables.  
      this.data = data;
      this.next = next;
    }


    // Sometimes we might want to create a node early on, and then
    // position it later in the code. This might be done for readability.
    // As a result, we'll create a constructor that takes only the data
    public Node (Object data)
    {
      this.data = data;
      this.next = null; // Notice the null reference.
    }


    // A Node isn't useful unless we can access the data its organizing.
    // This method will let us get to it.
    public Object getData() {
      return data;
    }


    // This will let us find the next Node in the list so that we can 
    // walk through the list as well as make changes in its organization.
    public Node getNext() {
      return next;
    }


    // We might want to insert a different item in front of this Node,
    // perhaps to add a Node, or perhaps just to reorganize things.
    public void setNext(Node next) {
      this.next = next;
    }

    /*
     * Notice that we have't defined a setData(). Some people would do
     * this, which would allow us to change the data associated with a 
     * Node. As a matter of my own personal philosophy, I don't want to
     * do this. Ideally, I think a Node should represent a specific 
     * position of a specific item with a LinkedList and should be thrown
     * away and recreated if either changes. This would prevent many
     * errors associated with changing things incorrectly and leaving the
     * list in an inconsistent state. 
     *
     * Unfortunately, we can't quite do that. We need to have a method
     * to setNext(). Without this method, changing the list would require
     * recreating each and every Node up to the point of the change. This
     * is becuase the changed or inserted Node would be a new Node. The
     * predecessor would then need to be changed to reference it, so it,
     * too, would need to be recreated. The same is true of its predecessor,
     * and then its predecessor, &c.
     *
     * As a result, we'll have a setNext() in our Node, but not a setData().
     * This, seems to me, to be a good compromise. Any change reuqires the
     * creation of a single node, but will not cascade. 
     */
  }
  

Implementing the LinkedList

The LinkedList class contains the methods used to store, manipulate, and remove data from the LinkedList. These behaviors are implemented by accessing Nodes within the LinkedList

  public class LinkedList {

    private class Node {
      // This class is implemented above
    }

    // The LinkedList's instance variables go here

    // The constructor and methods of the LinkedList go below them here.
  }
  

The LinkedList's Instance Variables

Recall from our discussion of yesterday that, in order to maintain a LinkedList, we must maintain a link to the first Node of the list. We called this reference the head of the list. We also decided that it would be a good idea to include a reference to the last Node in the list, so that we can easily add things at the end of the list. We caled this reference the tail reference. We also decided that a third reference that could "walk" the list would be useful. We called that one the index.

For right now, we're only going to worry about the head and tail. But, we'll come back to the index later.

  private Node head;
  private Node tail;
  

The LinkedList's Constructor

The job of the constructor is to initialize the object. In this case, this is really easy: set both references to null. There really isn't much else to do when constructing an empty list.

  public LinkedList () {
    head = tail = null; // Read as "set tail to null, then set head to tail"
  }

  

Other (Basic) Methods of the LinkedList

One thing that we'll want to do is to add items to the beginning of the list. Let's take a look at how to do that:

  // This adds an item to the beginning of the list
  public void addFirst (Object newObject) {
    // Note the parameter. Obviously, we need to know what to add

    // The following line creates a new Node containing a reference to
    // the newObject. Notice that the "next" reference of this
    // Node is being set to "head". This works out, because in an
    // empty list, it correctly makes the newNode's "next"
    // null. But, in an existing LinkedList, the newNode is correctly 
    // placed before the other Nodes, by placing them after it, that is
    // but setting its successor to the old head. The last step is to
    // reset the head reference to name the newNode. This can be done
    // within one line, as below, because the newNode is created and 
    // initialized before the assignment reassigns the value of head.
   
    head =  new Node (newObject, head);
  }
  

Another thing that we'll want to do is to add things to the end of the list. Let's take a look at the addLast() method's implementation:

  // This adds an item at the end of the list
  public void addLast (Object newObject) {
    
    // The first thing we want to do is to create the new Node. As before,
    // it names the user's newObject. Since it is being added at the end
    // of the list, we intialize its "next" to null. We can do this 
    // explicitly with the constructor as shown below, or by calling the
    // version of the constructor with just one parameter, the data.

    Node newNode = new Node (newObject, null);

    // This newNode needs to be inserted into the list. If there is a last
    // element, it should go there. Otherwise, we need to make it the
    // only item in the list. The difference is that, if it is the last
    // item in the list, the old tail's "next" needs to be set to reference
    // it. Otherwise, both "head" and "tail" need to be set to reference it.

    if (null == tail) {
      // the case where it is to become the only item in the list
      
      head = tail = newNode;
    }
    else {
      
      tail.setNext (newNode); // tack it on at the end.
      tail = tail.getNext();  // reset our understanding of the end.
    }

  }
  

The next method we are going to write today is going to search the list for an item and tell us whether or not it is within the list. It is important to realize that this search is going to be based on the equals() method defined within the user's object. We don't know how this is defined -- but trust that it is good for whatever their application might be.

  // This returns true if the specified item is found in the list adn false
  // otherwise
  public boolean contains (Object findMe) {
 
    // Notice the three different parts of the "for loop":
    // 1) The initialization: Start by declaring an index, whose scope
    //    will be this for loop, and initialize it to the same thing as 
    //    the head of the list. 
    // 2) The continuation: Start this loop, and continue into each iteration, 
    //    only if this index names a real Node, not null.
    // 3) The iteration: after each iteration of this loop, advance "index"
    //    by setting it to its successor
    for (Node index = head; null !=index; index = index.getNext() ) {
    
      // Now, we need to get a reference to the data within this node
      Object data = index.getData();

      // Now, we check to see if the data is the same as the one for which
      // we are looking

      if (data.equals(findMe)) 
        return true; // We've found it, so return "true"
    }

    // If we get here, it is becuase the "index" became null. That 
    // implies that we looked at each item in the list and didn't find
    // the item for which we were looking, so we return "false"

    return false;
  }
  

If we want to remove the first item from the list, this is reasonably straight-forward. We get a reference to it, so we can return it. Then, we reset the head to skip past it. The garbage collector does the rest.

  
// This removes the first item from the list. public Object removeFirst() { // If the list is empty, just return null if (null == head) return null; Object returnMe = head.getData(); // Get the first data item head = head.getNext(); // Advance the head to the next one return returnMe; // Return the item we removed }

Everything So Far

  public class LinkedList {

    private class Node {
      private Object data; // The Object organized by this node
      private Object next; // A reference to the next Node in the list

      // When we create a new Node, we'll usually know where it is
      // being placed within the list. Since we'll always know
      // the corresponding user-Object, we want a constructor with both. 
      public Node (Object data, Node next)
      {
        // Notice the use of "this" to shift the scope from the 
        // parameters to the instance variables.  
        this.data = data;
        this.next = next;
      }
  
  
      // Sometimes we might want to create a node early on, and then
      // position it later in the code. This might be done for readability.
      // As a result, we'll create a constructor that takes only the data
      public Node (Object data)
      {
        this.data = data;
        this.next = null; // Notice the null reference.
      }


      // A Node isn't useful unless we can access the data its organizing.
      // This method will let us get to it.
      public Object getData() {
        return data;
      }


      // This will let us find the next Node in the list so that we can 
      // walk through the list as well as make changes in its organization.
      public Node getNext() {
        return next;
      }


      // We might want to insert a different item in front of this Node,
      // perhaps to add a Node, or perhaps just to reorganize things.
      public void setNext(Node next) {
        this.next = next;
      }
    } // end of nested Node class

    // LinkedList's instance variables
    private Node head;
    private Node tail;


    // LinkedList's constructor
    public LinkedList () {
      head = tail = null; // Read as "set tail to null, then set head to tail"
    }


      public void addFirst (Object newObject) {
         // Note the parameter. Obviously, we need to know what to add

         // The following line creates a new Node containing a reference to
         // the newObject. Notice that the "next" reference of this
         // Node is being set to "head". This works out, because in an
         // empty list, it correctly makes the newNode's "next"
         // null. But, in an existing LinkedList, the newNode is correctly 
         // placed before the other Nodes, by placing them after it, that is
         // but setting its successor to the old head. The last step is to
         // reset the head reference to name the newNode. This can be done
         // within one line, as below, because the newNode is created and 
         // initialized before the assignment reassigns the value of head.
        
         head =  new Node (newObject, head);
       }


  public void addLast (Object newObject) {
                      
         // The first thing we want to do is to create the new Node. As before,
         // it names the user's newObject. Since it is being added at the end
         // of the list, we intialize its "next" to null. We can do this 
         // explicitly with the constructor as shown below, or by calling the
         // version of the constructor with just one parameter, the data.

         Node newNode = new Node (newObject, null);

         // This newNode needs to be inserted into the list. If there is a last
         // element, it should go there. Otherwise, we need to make it the
         // only item in the list. The difference is that, if it is the last
         // item in the list, the old tail's "next" needs to be set to reference
         // it. Otherwise, both "head" and "tail" need to be set to reference it.

         if (null == tail) {
           // the case where it is to become the only item in the list
           
           head = tail = newNode;
         }
         else {
           
           tail.setNext (newNode); // tack it on at the end.
           tail = tail.getNext();  // reset our understanding of the end.
         }

       }


       public Object removeFirst() {

         // If the list is empty, just return null
         if (null == head)
           return null;

         Object returnMe = head.getData(); // Get the first data item
         head = head.getNext(); // Advance the head to the next one
       
         return returnMe; // Return the item we removed
       
       }
  } // End of LinkedList class