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, itsprev
isnull
. 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, itsnext
isnull
.
null
is exactly what it sounds like - nothing. This means that you cannot refer tonull.getNext()
ornull.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
ClassInstance Variables
private class Node { private Object data; private Node next; private Node prev; //constructors and methods }prev
is just likenext
, except thatprev
refers to the node's predecessor.Constructors
Take a another look at the second two constructors. What's the scope of
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; }data
? What's the scope ofthis.data
? Remember thatthis
refers to the object you've just made with thenew
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
We could leave
public void setData (Object data) { this.data = data; }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 itsnext
andprev
references.As with singly-linked lists, the
public void setPrev (Node prev) { this.prev = prev; } public void setNext (Node next) { this.next = next; }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
Here,
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; }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.
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.

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).
public Object getHead()
{
if (null == head)
return null;
return head.getData();
}
public Object getTail()
{
if (null == tail)
return null;
return tail.getData()
}
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 */