CMPU 102, Spring 2006 Lecture 11

Iterators, Doubly-linked lists

Iterators

One way to iterate through the elements in a linked list is to write an explicit loop that visits each node of the list in turn:

Node<E> cur = list.head;
while (cur != null) {
    E payload = cur.payload;

    doSomethingWithElement(payload);

    cur = cur.next;
}

The problem with this approach is that the node data structure is a hidden implementation detail of the linked list class; clients of the linked list should not be exposed to such details.  Hiding implementation details from clients of a class is known as encapsulation, and is an important principle in software construction.

One way around the problem would be to define a functor interface that could be applied to elements of the list by a method of the list class.  For example:

public interface ElementFunctor<E> {
    public void apply(E element);
}

public class LinkedList<E> {
    Node<E> head;

    ...

    public void map(ElementFunctor<E> functor) {
        Node<E> cur = head;
        while (cur != null) {
            functor.apply(cur.payload);
            cur = cur.next;
        }
    }
}

Code wishing to traverse the list could provide an ElementFunctor object to be passed to the map method, which in turn would call the apply method of the functor once for each element in the linked list.  This is a useful solution to the problem, and would work well in many situations.  However, there are some difficulties.  First, we have to define a separate functor class for each kind of operation we want to perform over list elements.  (Anonymous classes can be useful for this purpose: see Weiss, pages 144-145.)  Another problem is that the apply method cannot directly refer to local variables and fields in the method that initiates the traversal.  (Again, anonymous classes can do this to a limited extent, but not as conveniently as the body of a loop could.)

An Iterator is an object whose purpose is to traverse the elements stored in a data structure.  Through the use of a common iterator interface, it is possible to have iterator classes for traversing any kind of data structure, without requiring clients of the data structures to be exposed to low-level implementation details such as list nodes.  Here is the Iterator interface defined in the java.util package:

public interface Iterator<E> {
    public boolean hasNext();
    public E next();
    public void remove();
}

The hasNext method returns true if the iterator has more elements to return.  The next method returns the next element in the collection.  Finally, the remove method removes the element most recently returned by next from the data structure.  (This is an optional method; iterators not supporting removal will throw UnsupportedOperationException.)

Objects implementing the Iterator interface can be created by all of the standard Java generic data structure classes (such as LinkedList and Vector) in order to support traversals.

Using an iterator is very simple.  Here's an example of code that uses an iterator returned by a list of Strings to print the value of each String:

List<String> list = ...

Iterator<String> i = list.iterator();
while (i.hasNext()) {
    String s = i.next();
    System.out.println(s);
}

Implementing an Iterator class

Say that we have a class SinglyLinkedList implementing a singly-linked list, and that it uses the SLLNode class as the list node type:

public class SLLNode<E> {
    E payload;
    SLLNode<E> next;
}

public class SinglyLinkedList<E> {
    SLLNode<E> head;

    ...
}

Here is how we can define an iterator class for SinglyLinkedList.  We will assume that a new iterator is created by passing its constructor a reference to the head node of the list (or null if the list is empty).  A field called nextNode in the iterator class keeps track of the next list node containing the element that will be returned by the next call to the next method.  The hasNext method also uses this field to check whether or not there is another element to return.

public class SLLIterator<E> implements Iterator<E> {
    Node<E> nextNode;

    public SLLIterator(Node<E> head) { nextNode = head; }

    public boolean hasNext() { return nextNode != null; }

    public E next() {
        if (nextNode == null) throw new NoSuchElementException();
        E result = nextNode.payload;
        nextNode = nextNode.next;
        return result;
    }

    public void remove() {
        throw new UnsupportedOperationException();
    }
}

Using this class, the SinglyLinkedList class can now easily support an iterator method that returns an iterator positioned at the beginning of the list:

public class SinglyLinkedList<E> {
    SLLNode<E> head;

    ...

    public Iterator<E> iterator() {
        return new SLLIterator<E>(head);
    }
}

Doubly-linked lists

Singly-linked lists have two important limitations, both of which step from the fact that here is no easy way to find a node's predecessor in the list.

  1. iterating backwards through a singly-linked list is not possible

  2. deleting a node is difficult (since it requires having a reference to the predecessor of the node being deleted)

In a doubly-linked list, each node has a reference to its predecessor:

Through use of the next and prev node fields, it is possible to immediately locate any node's predecessor and sucessor nodes.  This allows a reverse iterator to be implemented efficiently.  It is also possible to delete any node in the list in O(1) time.  As before, the last node in the list has a next field that contains null.  A symmetrical situation exists for the first node of the list; its prev field contains null.  The head and tail fields of the list object contain references to the first and last nodes of the list, respectively.

Circular lists

Circular lists are a variant of doubly-linked lists.  Rather than using separate head and tail fields, circular lists use a single head field.  Ordinarily, the lack of a tail field would make it necessary to traverse the entire list to find the last element of the list.  Circular lists address this problem by connecting the first and last elements in the list using the next and prev fields.  The first node in the list has the last node as its predecessor, and the last node in the list has the first node as its successor.  Here is an example:

The obvious feature is that when traversing a circular list in either direction, you will eventually come back to the node where you started.  Thus, a loop to traverse a circular list backwards (from the last node in the list to the first) might be written this way:

if (head != null) {
    Node<E> cur = head;
    do {
        cur = cur.prev;
        doSomethingWithNode(cur);
    } while (cur != head);
}

As long as we pay attention to this property, we can efficiently implement the same operations as a "normal" doubly-linked list, without requiring a tail field.  Of course, eliminating a single field per list is a very minor savings, so it might not be worth the extra coding complexity.