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 contentsThe 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.