CS136, Lecture 9

  1. Linked Lists (cont)
    1. Complexity of operations
      1. Other linked list representations
      2. Doubly-linked lists

Linked Lists (cont)

Complexity of operations

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 add(Object value)
    // post: adds value to beginning of list.
    {
        addToHead(value);
    }

    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.setNext(tail);
        } 
        else {
            temp.setNext(tail.next());
            tail.setNext(temp);
        }
        count++;
    }

    public void addToTail(Object value)
    // pre: value non-null
    // post: adds element to tail of list
    {
        addToHead(value);
        tail = tail.next();     // 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.setNext(temp.next());
            temp.setNext(null);  // helps clean things up; 
        }                                 // temp is free
        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.setNext(tail.next());
            tail = finger;
        }
        count--;
        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 != tail) && (!finger.value().equals(value)))
            finger = finger.next();
        return finger.value().equals(value);
    }

    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.setNext(previous.next().next());
            }
            // finger value free
            finger.setNext(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.
    {
    count = 0;
        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

public class DoublyLinkedListElement {
    Object value;                       // value held in elt
    DoublyLinkedListElement nextElement;    // successor elt
    DoublyLinkedListElement previousElement; // predecessor elt

public DoublyLinkedListElement(Object v,
                            DoublyLinkedListElement next,
                            DoublyLinkedListElement previous)
    // post: constructs new element with list
    //       prefix referenced by previous and
    //       suffix referenced by next
    {
        data = v;
        nextElement = next;
        if (nextElement != null)
            nextElement.previousElement = this;
        previousElement = previous;
        if (previousElement != null)
            previousElement.nextElement = this;
    }

    DoublyLinkedListElement(Object v)
    // post: constructs a single element
    {
        this(v,null,null);
    }

   public DoublyLinkedListElement next()
    // post: returns the element that follows this
    {
        return nextElement;
    }

    public DoublyLinkedListElement previous()
    // post: returns element that precedes this
    {
        return previousElement;
    }

    public Object value()
    // post: returns value stored here
    {
        return data;
    }

    public void setNext(DoublyLinkedListElement next)
    // post: sets value associated with this element
    {
        nextElement = next;
    }

    public void setPrevious(DoublyLinkedListElement previous)
    // post: establishes a new reference to a previous value
    {
        previousElement = previous;
    }

    public void setValue(Object value)
    // post: sets a new value for this object
    {
        data = value;
    }
}

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 add(Object value)
   // post: adds value to beginning of list.
   {
        addToHead(value);
   }

    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.setPrevious(null);
        else 
             tail = null; // remove final value
        temp.setNext(null);// 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.setNext(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().setNext(finger.next());
            else 
                head = finger.next();
            // fix previous field of following element
            if (finger.next() != null)
                finger.next().setPrevious(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.