CS136, Lecture 8

  1. List
    1. Vector Implementation
    2. Linked List Implementation

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

Vector Implementation

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