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 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 value; // value stored in this element SinglyLinkedListElement next; // ref to next element // constructors SinglyLinkedListElement(Object v, SinglyLinkedListElement rest) // post: constructs a new element with value v, // followed by list rest { value = v; next = rest; } SinglyLinkedListElement(Object v) // post: constructs a new element of a list with value v // but with nothing attached. { this(v,null); } }
This class is NOT declared public, as it is not intended to be used by objects outside of the structure package. Similarly, the fields have default visibility, which makes them visible to other objects of the package.
My personal preference would have been to make them protected and to provide methods to set and return their values.
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 addToHead(Object value) // post: adds 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.next = 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.next = null; } 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.next = 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.
Compare time complexities of operations in implementations:
size(), isEmpty(), peek() // O(1) in both tailPeek(), removeFromTail() // O(1) in Vector, O(n) in Linked clear(); addToHead(Object value); removeFromHead(); // O(n) in Vector, O(1) in Linked contains(Object value); remove(Object value); // O(n) in both addToTail(Object value); // O(n) in Linked, // varies in Vector - usually O(1)
Comparison of space complexities a bit tricky.
If list of size n kept in vector, then if "tight fit" (underlying array has exactly n elements), then need n*words(value), where words(value) is the amount of space necessary to hold a value stored in the list (including the initial reference). But underlying array may be much, much larger (remember underlying array never shrinks), so may use much more space.
Linked list representation is more predictable: space for count & head, and then for each node, space for value plus one reference.
Linked always O(n), Vector usually so unless really shrunk during its lifetime.
Note if didn't keep count field, size operation would become O(n) in Linked list, but would save time to update count in remove and add operations
Notice that tailPeek, addToTail, and removeFromTail are the only operations that are more expensive in the linked representation than Vector.
The problem is that it takes O(n) time to find the end. We can overcome this by adding a new field to the class: tail, which should serve as a pointer to the tail of the list.
An empty list has head=tail=null, a list w/one elt has head = tail = reference to that elt, while in all other cases, head != tail.
What is the cost of this? Adds complexity to add and remove methods since must worry about resetting tail field. Even addToHead may have to reset tail field (why?).
It also doesn't help with removeFromTail since need predecessor!
Can simplify further by noticing that tail node of list has wasted next field = null.
Why not use that field to point to beginning of list, then don't need head field?
Called Circularly linked list. Head always found as tail.next!
public class CircularList implements List { protected SinglyLinkedListElement tail; protected int count; public CircularList() // pre: constructs a new circular list { tail = null; count = 0; } public void addToHead(Object value) // pre: value non-null // post: adds element to head of list { SinglyLinkedListElement temp = new SinglyLinkedListElement(value); if (tail == null) { tail = temp; tail.next = tail; } else { temp.next = tail.next; tail.next = temp; } count++; } public void rotate() // pre: !isEmpty() // post: requeues element from head to tail { tail = tail.next; } public void addToTail(Object value) // pre: value non-null // post: adds element to tail of list { addToHead(value); rotate(); // moves new from head to tail } public Object peek() // pre: !isEmpty() // post: returns value at head of list { return tail.next.value; } public Object tailPeek() // pre: !isEmpty() // post: returns value at tail of list { return tail.value; } public Object removeFromHead() // pre: !isEmpty() // post: returns and removes value from head of list { SinglyLinkedListElement temp = tail.next; // ie. the head of the list if (tail == tail.next) // 1 elt in list tail = null; else tail.next = temp.next; count--; return temp.value; } public Object removeFromTail() // pre: !isEmpty() // post: returns and removes value from tail of list { Assert.pre(!isEmpty(),"The list is not empty."); SinglyLinkedListElement finger = tail; while (finger.next != tail) finger = finger.next; // finger now points to second-to-last value SinglyLinkedListElement temp = tail; if (finger == tail) tail = null; else { finger.next = tail.next; tail = finger; } return temp.value; } public boolean contains(Object value) // pre: value != null // post: returns true if list contains value, else false { if (tail == null) return false; SinglyLinkedListElement finger; finger = tail.next; while ((finger != null) && (!finger.value.equals(value))) finger = (finger == tail) ? null : finger.next; return finger != null; } public Object remove(Object value) // pre: value != null // post: remove & returns element equal to value, or null { if (tail == null) return null; SinglyLinkedListElement finger = tail.next; SinglyLinkedListElement previous = tail; int compares; for (compares = 0; (compares < count) && (!finger.value.equals(value)); compares++) { previous = finger; finger = finger.next; } if (finger.value.equals(value)) { // an example of the pigeon-hole principle if (tail == tail.next) tail = null; else { if (finger == tail) tail = previous; previous.next = previous.next.next; } // finger value free finger.next = null; // to keep things disconnected count--; // fewer elements return finger.value; } else return null; } public int size() // post: returns number of elements in list { return count; } public boolean isEmpty() // post: returns true if no elements in list { return tail == null; } public void clear() // post: removes all elements from list. { tail = null; } }With this implementation, addToTail is now O(1).
Cost is now takes one extra dereference (following reference) to get to head.
Only contains, remove, and removeFromTail are still O(n).
Contains and remove involve searches and seem likely always to be O(n) (unless attempt to keep list in order and do binary search - which has its own problems - come back to it later!).
However, why can't we make removeFromTail O(1)? The problem is that we need to know the predecessor in order to delete an element from the list.
Unfortunately references only go from the front to the back of the list. Why not put them in the other direction instead? Then it would be harder to delete from the front.
Why not put references in both directions!?!
class DoublyLinkedListElement { Object value; // value held in elt DoublyLinkedListElement next; // successor elt DoublyLinkedListElement previous; // predecessor elt DoublyLinkedListElement(Object v, DoublyLinkedListElement next, DoublyLinkedListElement previous) // post: constructs new element with list // prefix referenced by previous and // suffix reference by next { value = v; this.next = next; if (this.next != null) this.next.previous = this; this.previous = previous; if (this.previous != null) this.previous.next = this; } DoublyLinkedListElement(Object v) // post: constructs a single element { this(v,null,null); } }With this defined, it is now easy to define a doubly-linked list. This time we'll keep track of both the first (head) and last (tail) elements of the list so can get to tail quickly.
public class DoublyLinkedList implements List { protected DoublyLinkedListElement head; protected DoublyLinkedListElement tail; protected int count; public DoublyLinkedList() // post: constructs an empty list { head = null; tail = null; count = 0; } public void addToHead(Object value) // pre: value is not null // post: adds element to head of list { // construct a new element, making it the head head = new DoublyLinkedListElement(value, head, null); // fix tail, if necessary if (tail == null) tail = head; count++; } public Object removeFromHead() // pre: list is not empty // post: removes first value from list { Assert.pre(!isEmpty(),"List is not empty."); DoublyLinkedListElement temp = head; head = head.next; if (head != null) head.previous = null; else tail = null; // remove final value temp.next = null;// helps clean things up; temp is free count--; return temp.value; } public void addToTail(Object value) // pre: value is not null // post: adds new value to tail of list { // construct new element tail = new DoublyLinkedListElement(value, null, tail); // fix up head if (head == null) head = tail; count++; } public Object removeFromTail() // pre: list is not empty // post: removes value from tail of list { Assert.pre(!isEmpty(),"List is not empty."); DoublyLinkedListElement temp = tail; tail = tail.previous; if (tail == null) head = null; else tail.next = null; count--; return temp.value; } public Object peek() // pre: list is not empty // post: returns first value in list. { return head.value; } public Object tailPeek() // pre: list is not empty // post: returns last value in list. { return tail.value; } public boolean contains(Object value) // pre: value not null // post: returns true iff value is in the list { DoublyLinkedListElement 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. List can be empty. // post: first element matching value is removed from list { DoublyLinkedListElement finger = head; while (finger != null && !finger.value.equals(value)) finger = finger.next; if (finger != null) { // fix next field of previous element if (finger.previous != null) finger.previous.next = finger.next; else head = finger.next; // fix previous field of following element if (finger.next != null) finger.next.previous = finger.previous; else tail = finger.previous; count--; // fewer elements return finger.value; } // Didn't find value return null; } public int size() // post: returns the number of elements in list { return count; } public boolean isEmpty() // post: returns true iff the list has no elements. { return size() == 0; } public void clear() // post: removes all the elements from the list { head = tail = null; count = 0; } }RemoveFromTail is now O(1), but tradeoff is now all addition and removal operations must set one extra pointer in element. Must also worry about changing head and tail of the list.
Having looked at both circularly-linked list and doubly-linked list, the next obvious variation to look at is the doubly-linked circularly-linked list. That will be part of your next homework assignment!
With singly-linked circular list we kept track of tail rather than the head of the list - why? With doubly-linked circular list we just keep track of the head since it is easy to find the tail.