Limitations of Singly-Linked Lists

As compared with Vectors, linked lists have some really nice properties -- and some big limitations. Perhaps the most important properties of the linked list is that it can grow and shrink smoothly, while providing fast sequential access.

Vectors, by contrast, provide not only fast sequently access -- but also fast indexed access. The cost comes in two parts. The first is that growing a vector results in a large, probably unexpected penalty, as the internal array is reallocated and copied. The second is that insertion or removal in the middle of the list, unlike the linked list, requires the movement of each and every subsequent node in the lists. This is also a painful penalty.

But, looking back to linked lists for a moment, we had another penalty -- the deletion of a node from a known position, or insert the node before the known position resulted in a partial traversal of the list -- it was necessary to find the predecessor of the know position and there was no direct way to move backward. This was a costly operation.

Today, we are discussing doubly linked lists (yes, they will be on the test :-). These lists eliminate this penalty, by providing direct access to each node's predecessor -- but, again, at a cost: an additional reference in each node.

Doubly linked lists will have the familiar "next" and "data" references, as well as a new reference "prev". Prev works, basically, symetrically to "next". It does, of course, as you might imagine, add some special cases, especially at the beginning of the list, as well.

Shattering a Fallacy

Below is one of my favorite white lies. Intro instructors all across the world tell this one to their students -- don't believe them!
Vectors waste more space than linked lists, because they double in size when they grow, so they can be half-full. So, linked lists are more space efficient.

This isn't true. Think about it this way. A vector takes one reference per object, to keep the object in the container. Basically, it is an array of object references, with one reference per object. So, if it is half full, we double this cost to two references per object.

A singly linked list, by contrast, maintains the same two references per object -- "data" and "next". So, the best and worst cases for linked lists are the same as the worst case for a Vector. The best case for the Vector wins by 2:1!

And, if we consider a doubly linked list, with 3 references per object, the cost is even higher -- 50% higher than a singly linked list and 200% higher than for a Vector.

Of course, this can change a little bit if removals are thrown into the mix, because linked lists will shrink on removal, but Vectors will not (even though they grow on inserts).

I guess I should also point out that you can change the growth policy on a Vector from 2:1, to your choice of linear metrics -- but then you risk copying the same data many times.

Doubly-Linked Lists Introduction

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 addAfterIndex(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();
  }

DoubleLinkedListNode Implementation

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;
  }
}

DoublyLinkedList Implementation

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 */