public class SinglyLinkedList implements List { protected SinglyLinkedListElement head; // first elt protected int count; // list size ... 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.