public interface List extends Container {
public Iterator elements(); // ignore for now!
// post: returns an iterator allowing
// ordered traversal of elements in list
public int size(); // from Container
// post: returns number of elements in list
public boolean isEmpty(); // from Container
// post: returns true iff list has no elements
public void clear(); // from Container
// post: empties list
public void add(Object value);
// post: value is added to beginning of list (see addToHead)
public void addToHead(Object value);
// post: value is added to beginning of list
public void addToTail(Object value);
// post: value is added to end of list
public Object peek();
// pre: list is not empty
// post: returns first value in list
public Object tailPeek();
// pre: list is not empty
// post: returns last value in list
public Object removeFromHead();
// pre: list is not empty
// post: removes first value from the list
public Object removeFromTail();
// pre: list is not empty
// post: removes the last value from the list
public boolean contains(Object value);
// post: returns true iff list contains an object equal
// to value
public Object remove(Object value);
// post: removes and returns element equal to value
// otherwise returns null
}
We can imagine other useful operations on lists, such as return nth element, etc., but we'll stick with this simple specification for now.
The text has a simple example of reading in successive lines from a text and adding each line to the end of a list if it doesn't duplicate an element already in the list. This is easily handled with the operations provided.
public class VectList implements List
{
protected Vector listElts;
public VectList()
{
listElts = new Vector();
}
....
}
How expensive would each of the operations be (worst case) if the VectList contains n elements?
Some are easy. Following are O(1). Why?
size(), isEmpty(), peek(), tailPeek(), removeFromTail()
Others
take more thought:
clear(); // O(n) currently, because reset all slots
// to null, but could be O(1)
addToHead(Object value); //O(n) - must move contents
removeFromHead(); //O(n) - must move contents
contains(Object value); //O(n) - must search
remove(Object value); //O(n) - must search & move contents
The
last is the trickiest:
addToTail(Object value);
If
the vector holding the values is large enough, then it is clearly O(1), but if
needs to increase in size then O(n). If use the doubling strategy then saw
this is O(1) on average, but O(n) on average if increase by fixed amount.All of the other operations have the same "O" complexity in the average case as for the best case.
First provide SinglyLinkedListElement class representing the nodes:
class SinglyLinkedListElement {
// these public fields protected by private class
Object data; // value stored in this element
SinglyLinkedListElement nextElement;
// ref to next element
// constructors
SinglyLinkedListElement(Object v, SinglyLinkedListElement next)
// post: constructs a new element with value v,
// followed by next
{
data = v;
nextElement= next;
}
SinglyLinkedListElement(Object v)
// post: constructs a new element of a list with value v
// but with nothing attached.
{
this(v,null);
}
public SinglyLinkedListElement next()
// post: returns reference to next value in list
{
return nextElement;
}
public void setNext(SinglyLinkedListElement next)
// post: sets reference to new next value
{
nextElement = next;
}
public Object value()
// post: returns value associated with this element
{
return data;
}
public void setValue(Object value)
// post: sets value associated with this element
{
data = value;
}
public String toString()
// post: returns string representation of element
{
return "<SinglyLinkedListElement: "+value()+">";
}
}
Bit like an association, but association to itself - i.e., it is recursive!
The actual linked list implementation is pretty straightforward, but to understand the code you MUST draw pictures to see what is happening!
public class SinglyLinkedList implements List {
protected SinglyLinkedListElement head; // first elt
protected int count; // list size
public SinglyLinkedList()
// post: generates an empty list.
{
head = null;
count = 0;
}
public void add(Object value)
// post: adds value to beginning of list.
{
addToHead(value);
}
public void addToHead(Object value)
// post: add value to beginning of list.
{
// note the order that things happen:
// head is parameter, then assigned!!!
head = new SinglyLinkedListElement(value, head);
count++;
}
public Object removeFromHead()
// pre: list is not empty
// post: removes and returns value from beginning of list
{
SinglyLinkedListElement temp = head;
head = head.next(); // move head down the list
count--;
return temp.value;
}
public void addToTail(Object value)
// post: adds value to end of list
{
// location for the new value
SinglyLinkedListElement temp =
new SinglyLinkedListElement(value,null);
if (head != null)
{
// pointer to possible tail
SinglyLinkedListElement finger = head;
while (finger.next() != null)
finger = finger.next();
finger.setNext(temp);
}
else
head = temp;
count++;
}
public Object removeFromTail()
// pre: list is not empty
// post: last value in list is returned
{
// keep two ptrs w/ previous one elt behind finger
SinglyLinkedListElement finger = head;
SinglyLinkedListElement previous = null;
Assert.pre(head != null,"List is not empty.");
while (finger.next() != null) // find end of list
{
previous = finger;
finger = finger.next();
}
// finger is null, or points to end of list
if (previous == null) // list had 1 or 0 elements
head = null;
else // pointer to last element reset to null.
previous.setNext(null);
}
count--;
return finger.value();
}
public Object peek()
...
public Object tailPeek()
// find end of list as in removeFromTail
public boolean contains(Object value)
// pre: value is not null
// post: returns true iff value is found in list.
{
SinglyLinkedListElement finger = head;
while (finger != null && !finger.value().equals(value))
finger = finger.next();
return finger != null;
}
public Object remove(Object value)
// pre: value is not null
// post: removes 1st element with matching value, if any.
{
SinglyLinkedListElement finger = head;
SinglyLinkedListElement previous = null;
while (finger != null && !finger.value().equals(value))
{
previous = finger;
finger = finger.next();
}
// finger points to target value
if (finger != null) {
// we found the element to remove
if (previous == null) // it is first
head = finger.next();
else // it's not first
previous.setNext(finger.next());
count--;
return finger.value();
}
// didn't find it, return null
return null;
}
public int size()
// post: returns the number of elements in list
{
return count;
}
public boolean isEmpty()
// post: returns true iff the list is empty
{
return size() == 0;
}
public void clear()
// post: removes all elements from the list
{
head = null;
count = 0;
}
}
Notice
all of the effort that went on in the methods to take care of boundary cases -
adding or removing the last element or removing elt found in list. Most common errors in working with linked structures are ignoring these cases.