Return to the lecture notes index

Lecture #13 (February 15, 2006)

Doubly Linked Lists

Today we looked at how to implement Doubly Linked Lists by altering the regular LinkedList class the we worked on last week. Now, instead of each Node containing an item and a reference to the next node in the list, the Nodes now contain an additional reference to the previous node in the list.

First, lets look at the new code for the Node class.


private class Node {

  private Object item;
  private Node next
  private Node prev; //Computer Science convention to abbr. this

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

  public Node (Object item, Node next) {
    this.item = item;
    this.next = next;
    this.previous = null;
  }

  public Node (Object item, Node prev, Node next) {
    this.item = item;
    this.next = next;
    this.prev = prev;
  }

  public Object getItem() {
    return item;
  }

  public Node getNext() {
    return next;
  }

  public Node getPrev() {
    return prev;
  }

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

  public void setPrev (Node prev) {
    this.prev = prev;
  }

  public String toString() {
    return ("" + prev.getItem() + "<---" + item +
       "--->" + next.getItem());
  }

  public boolean equals(Object o) {
    Node n = (Node)o;
    if(! item.equals(n.getItem()))
      return false;

    if(next!= n.getNext())
      return false;

    if (prev != n.getPrev())
      return false;

    return true;
  }
}

This code should look pretty similar to the regular Linked List node, except with an extra constructor and some new accessors and mutators. Also, we adjusted the toString and equals method appropriately.

Now lets look at a few methods that have been changed to work for doubly linked lists. Lets look at addHead.

public void addHead (Object o) {

  if (o==null)
    return;

  Node newNode = new Node (o, null, head);
  
  if (head == null) {
    head = newNode;
    tail = newNode;
    count++;
    return;
  }

  
  newNode.setNext(head);
  head.setPrev(newNode);
  head = newNode;
  count++;
}

This is almost exactly the same, except now, we need to set the new second element's previous reference to point to the new head.

Now lets look at a method that will really take advantage of the previous references. Here is the code for the reverseToString() method we wrote. The Doubly Linked lists allows us to iterate over the list backwards, something which would have been extremely difficult and innefficient to do with a regular Linked List.

public String reverseToString() {
  String retString = "Count = " + count + NL;
  for ( Node index = tail; index!=null; index = index.getPrev())
    retString += item.getItem() + NL;

  return retString;
}

Another area where the Doubly Linked list is nice is removing items. Since we have a reference to the previous node, we can now iterate right to the node we want to remove. In a regular linked List, we always had to be looking one node ahead of ourselves. Now, its slightly easier.


public Object removeNth(int n) {
  if ((n<0) || (n >= count) )
    return null;

  if(n==0)
    return removeAtHead();

  Node index = head;
  for(int step = 1; step <= n; step++) {
    index = index.getNext();
  }

  Object temp = index.getItem();
  
  if(tail==index) {
    tail = index.getPrevious();
    tail.setNext(null);
    count--;
    return temp;
  }

  index.getPrev().setNext(index.getNext());
  index.getNext().setPrev(index.getPrev());
  count--;

  return temp;
}

public Object removeItem (Object o) {

  // If list is empty, return null
  if (count==0)
    return null;

   // If its the 0th item take care of it
   if(head.getItem().equals(o)) {
    return removeAtHead();
   }

   // Walk the list until we find it or hit end
   Node index = head;
   while ( (index!=null) && (!index.getItem().equals(o)) )
      index = index.getNext();

  // If we hit the end, return null
  if (index == null) {
    return null;
  }

  // If it is the tail, handle as special case
  if (index == tail) {
    tail = tail.getPrev();
    tail = tail.setNext(null);
    count--;
    return index.getItem();
  }

  // If found it, save it, then remove it, decrement count, then return it
  index.getPrev().setNext(index.getNext());
  index.getNext().setPrev(index.getPrev());
  count--;
  return index.getItem();
}

In the end, there isnt anything you can do with a Doubly Linked List that you can't do with a Singly Linked List. Also note that Doubly Linked Lists take up more space than a regular linked list. However, certain operations are more efficient with a Doubly Linked List.

For example, removing something at the tail of the list. With a singly Linked List, this is an O(n) operation, since you need to iterate all the way through to find the second to last item. However, using a doubly linked list, this can be done in O(1), or constant time.

For other operations, even if it doesnt give you faster code, the algorithm becomes much more intuitive and easier to write. Good examples of this are the reverseToString method and the generic remove methods, both of which can be done in O(n) using either Linked List implementation, but its easier to understand in a doubly Linked List.

Even though the extra storage cost of a doubly Linked List is generally not a large factor, you should still usually use Singly Linked Lists unless there is a specific reason why a Doubly Linked List will be more efficient.