CS136, Lecture 10

    1. List
      1. Vector Implementation
      2. Linked List Implementation
      3. Other linked list representations
      4. Doubly-linked lists

List

We will begin our study of data structures with lists. These are structures whose elements are in a linear order.

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.

VectorImplementation

Suppose we decided to implement List using a vector:

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.

Linked List Implementation

Linked list is composed of series of nodes, each of which has a reference to the next.

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

Other linked list representations

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!?!

Doubly-linked lists

The following class is hidden in file DoublyLinkedList.java

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.