Return to the lecture notes index

Lecture #8 (September 19, 2002)

Tests, Tests, Everywhere

The class decided to hold a review session Sunday evening at 8:00PM in the cluster -- see ya there! Everyone was encouraged to review my old exams on the Web, focusing ont he linked lsit and vector questions, de-emphasizing Java syntax (remember, the mission of 15-200 has changed).

I volunteer you guys to beta-test a working-draft of the end of semester exam. It won't be the same question -- and the format is a draft, not the final product. But, it should still be good practice. We'll do that during next week's recitation. It will involve writing methods internal to a singly linked list, without the benefit of other linked list methods.

Limitations of Singly-Linked Lists

One major advantage of linked lists over vectors is that we can remove an individual node, and the garbage collector will eventually pick it up. We can't reduce the size of a vector.

A downside of linked lists is that we don't have indexed access to the nodes. If I want a node in the middle of the list, I have to start from the beginning of the list.

Linked lists have two other problems. The first is that we can't go backwards. The reference in each node names only the next item in the list.

The second problem is that if we need to remove, say, node b, our index must reference node a, and then we must refer to index.getNext().getNext() in order to connect node a's "next" reference to node c (dereferencing node b and effectively removing it from the linked list).

Is there another way? There is, but it requires a list with one extra reference in each node, a doubly-linked list.  3

Doubly-Linked Lists

Doubly-Linked lists are lists which contain slightly different nodes than the ones found in singly-linked lists. Each node references its predecessor as well as its successor. This allows us to look and move both forward and backward in the list.

Here's a doubly-linked list. Each node has an extra reference, which we will call prev, to the node that comes before it in the linked list (its predecessor).

Take a look at Node b. It's prev reference refers to Node a, and its next reference refers to Node c.

Look at Node a. The dashed line in Node a's prev signifies the fact that, because Node a is the first node in the list, its prev is null. Look at Node d. The dashed line in Node d's next signifies the fact that, since Node d is at the end of the list, its next is null.

 

 

null is exactly what it sounds like - nothing. This means that you cannot refer to null.getNext() or null.getPrev()!!!

The great thing about doubly-linked lists is that, if you'd like to, say, remove the node in the above list containing "Mary", you can set index to refer directly to node b and delete it. How do you do this?

 

 

The thing to notice here is that there is no longer a reference to Node b, because Nodes a and c have been reset to reference each other. Index has been reset to refer to the first node in the list.

Let's look at the doubly-linked list class. It's much like the singly-linked list class. The nodes have an extra reference, a reference to their predecessors in the list. And the methods are different because we now have one more reference to worry about.

The Node Class

Instance Variables

private class Node
{
    private Object data;
    private Node next;
    private Node prev;
    
    //constructors and methods
}
prev is just like next, except that prev refers to the node's predecessor.

Constructors

    public Node()
    {
      data = null
      next = null;
      prev = null;
    }

    public Node (Object data)
    {
      this.data = data;
      next = prev = null;
    }

    public Node (Object data, Node prev, Node next)
    {
      this.data = data;
      this.next = next;
      this.prev = prev;
    }
Take a another look at the second two constructors. What's the scope of data? What's the scope of this.data? Remember that this refers to the object you've just made with the new operator. We want to have meaningful variable names, so these variables have the same name. Just keep the distinction between the data being passed into the method and the instance variable of the object.

Accessors

    public Object getData ()
    {
      return data;
    }
    
    public Node getPrev()
    {
    	return prev;
    }
    
    public Node getNext()
    {
    	return next;
    }

Mutators

	
public void setData (Object data)
{
	this.data = data; 
}
We could leave setData() out of the class definition, because we can always just create a new node every time we want to change something. But that would be more work. When we make a new node, we'll want to set its next and prev references.
    
    public void setPrev (Node prev)
    {
       this.prev = prev; 
    }

    public void setNext (Node next)
    {
       this.next = next; 
    }    

As with singly-linked lists, the Node class is a private subclass of the doubly-linked list class. Here's the rest of the doubly-linked list class. The doubly-linked list class is like the singly-linked list class - except that there's one more reference to worry about.

Instance Variables

  
  private Node head;
  private Node tail;
  private Node index;

Constructor

  public DoublyLinkedList()
  {
    head = tail = index = null;
  }

Adding a Node to the Beginning of the List

  public void addHead(Object data)
  {
    // Create new node
    Node newNode = new Node (data, null, head);

    // Special case: Empty List
    if (null == head)
    {
      head = tail = index = newNode;
      return;
    }

    // Common case
    head.setPrev(newNode);
    head = newNode;
  }
 
Here, newNode has been added to the front of the list.
 
 


Adding a Node to the End of the List

  public void addTail (Object data)
  {
    // Create new node
    Node newNode = new Node (data, tail, null);
 
    // Special case: Empty list
    if (tail == null)
    {
      tail = head = index = newNode;
      return;
    }

    // Common case
    tail.setNext (newNode);
    tail = newNode;
  }
 
Here, newNode has been added to the end of the list.
 

Adding a Node in the Middle of the List

  
  public void addBeforeIndex(Object data) throws IndexException
  {
    // Special case: Index is null
    if (null == index)
      throw new IndexException();

    // Create new node
    Node newNode = new Node (data, index, index.getNext());

    // Special case: index is tail (we could call addTail)
    if (index == tail)
    {
      index.setNext(newNode);
      tail = newNode;     
      return;
    }

    // Common Case
    index.getNext.setPrev (newNode);
    index.setNext(newNode);
  }

  public void addBeforeIndex(Object data)
  {
    // Special case: Index is null
    if (null == index)
      throw new IndexException();

    // Create new node
    Node newNode = new Node (data, index.getPrev(), index);

    //Special case: Index is head (we could call addHead)
    if (head == index)
    {
      index.setPrev (newNode);
      head = newNode;
      return;
    }

    // Common case
    index.getPrev.setNext (newNode);
    index.setPrev (newNode);
}
 
Here, newNode has been added between nodes b and c. Index has been reset 
to the beginning of the list.




Setting index Back to the Beginning of the List

  public void resetIndex()
  {
    index = head;
  }
You'll want to do this after you've finished deleting a node, because, if index still refers to your "deleted" node, you haven't deleted the node (because index still refers to it). Linked lists don't have the same indexing capabilities that vectors have, but we can write methods to compensate for this weakness.

Accessing the First Element in the List

  public Object getHead()
  {
    if (null == head) 
      return null;
    return head.getData();
  }

Accessing the Last Element in the List

  public Object getTail()
  {
    if (null == tail)
      return null;
    return tail.getData()
  }

Accessing the Element Referenced by Index

  public Object getIndexedData()
  {
    if (null == index)
      return null;
    return index.getData();
  }

Modifying the SinglyLinkedList Code from Class

As an exercise in thinking through LinkedLists, we modified the code from last class to convert it from a singly linked list to a doubly linked list.

We chose to modify the code for the two classes directly. But we could also have, arhuably should have, extended the other class using inheritence. This would allow us to directly reuse some of the common code and clearly express the relationship, "A doubly linked list is a linked list".

But, as it turns out, we wouldn't actually get to reuse as much as we might hope -- and this way matches the end-of-semester exam. It is also good practice for next week's exam.

Modifying the SinglyLinkedList Code From Class: DLinkedListNode

public class DLinkedListNode 
{
  private Comparable data;
  private DLinkedListNode next;
  private DLinkedListNode prev;
  
  public DLinkedListNode()  
  {
    data = null;
    next = null;
    prev = null;
  }  
  
  public DLinkedListNode (Comparable data, DLinkedListNode prev,
                          DLinkedListNode next) 
  {
   this.data = data; 
   this.next  = next;
   this.prev = prev;
  }
  
  public DLinkedListNode (Comparable data)
  {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
  
  public void setNext (DLinkedListNode next)
  {
    this.next = next;
  }
  
  public void setPrev (DLinkedListNode prev)
  {
    this.prev = prev;
  }
  
  public Comparable getData()
  {
    return data;
  }
  
  public DLinkedListNode getNext()
  {
    return next;
  }
  
  public DLinkedListNode getPrev()
  {
    return prev;
  }
}

Modifying the SinglyLinkedListCode from Last Class: DLinkedList

public class DLinkedList { private DLinkedListNode head; private DLinkedListNode tail; private DLinkedListNode index; public class DLinkedListException extends Exception { public DLinkedListException (String msg) { super(msg); } } public DLinkedList() { head = tail = index = null; } public void prepend (Comparable data) { head = new DLinkedListNode (data, null, head); if (head.getNext() != null) head.getNext().setPrev(head); if (null == tail) tail = head; if (null == index) index = head; } public void append (Comparable data) { if (null == tail) head = index = tail = new DLinkedListNode(data); else { tail.setNext (new DLinkedListNode (data,tail,null)); tail = tail.getNext(); } } public void resetIndex() { index = head; } public Comparable getIndex() throws DLinkedListException { if (null == index) throw new DLinkedListException ("Null index in getIndex()"); return index.getData(); } public void advanceIndex() throws DLinkedListException { if ((index == null) || (null == index.getNext())) throw new DLinkedListException ("Null index in advanceIndex()"); index = index.getNext(); } public void reverseIndex() throws DLinkedListException { if ( (null == index) || (index == head) ) throw new DLinkedListException ("Index is null or has no predecessor"); index = index.getPrev(); } public Comparable deleteAtIndex() throws DLinkedListException { Comparable data; try { data = index.getData(); reverseIndex(); // data = index.getNext().getData(); // Ugly, but avoids NPE index.setNext(index.getNext().getNext()); index.getNext().setPrev(index); } catch (NullPointerException npe) { throw new DLinkedListException ("index is null; can't delete"); } catch (DLinkedListException lle) { // No predecessor; first node in list head = index.getNext(); if (head != null) head.setPrev(null); } // Index was the tail if (index.getNext() == null) { tail = index; } // Move index to the one after the one we're deleting index = index.getNext(); return data; } public Comparable removeNth (int n) // beginning with 0th throws DLinkedListException { DLinkedListNode index; // Don't destroy user's index int count; if (null == head) throw new DLinkedListException ("Can't delete from empty list"); if (n == 0) { if (tail == head) tail = null; if (index == head) index = null; head = head.getNext(); if (head != null) head.setPrev (null); } else { try { for (index=head, count=0; count <= n; count++, index=index.getNext()) ; } catch (NullPointerException npe) { throw new DLinkedListException ("Less than n nodes on removeNth"); } if (index == tail) throw new DLinkedListException ("Less than n nodes on removeNth"); if (this.index == index) this.index = null; if (tail == index) tail = index.getPrev(); // same as tail=tail.getPrev() index.getPrev().setNext(index.getNext()); if (tail != index) index.getNext().setPrev(index.getPrev()); } } } /* DLinkedList */