CS 136 - Lecture 9

  1. Linked List Implementations
    1. Singly Linked Lists
    2. Circularly Linked Lists

Linked List Implementations


Singly Linked Lists

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

First provide SinglyLinkedListElement class representing the nodes:
 

public class SinglyLinkedListElement {

    protected Object data;        // value stored in this element
    protected SinglyLinkedListElement nextElement; 
                        // ref to next element

// constructors

    public SinglyLinkedListElement(Object v, SinglyLinkedListElement next)
    // post: constructs a new element with value v,
    //       followed by next
    {
        data = v;
        nextElement= next;
    }

    public 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()+">";
    }
}

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 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 element
            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! (Recall the CircularVector class you implemented and the special cases you needed to handle when removing elements.)


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.

Note. If we 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.


Circularly Linked Lists

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;
   }
}

Why not put references in both directions!?!