07: Intro to Linked Lists

Welcome

Announcements

Quiz makeups: Generally all makeups must be completed within a week of a quiz. We’ll make an exception for the first quiz but in the future, if you miss a quiz, you need to make it up that week unless you are totally out of school due to illness or whatnot. Arrange your makeup with your TA.

Check your grades in Moodle please!

Write your names legibly please! The TAs were unable to read some people’s names on the discussion sheets. You won’t get credit if we cannot read your name.

Assignment 04 guidance

A04 requires that your code be able to compare fragments for semantic equality. You, the programmer, must decide if some “other” object is equal to the current Fragment. One quick-n-dirty way to do it is as follows, assuming your only instance variable is n:

public boolean equals(Object o) {
    if (!(o instanceof Fragment)) {
        return false;
    }

    Fragment f = (Fragment)o;
    return this.n.equals(f.n);
}

You can also ask Eclipse to write a more thorough equals method for you, using the Source -> Generate hashCode() and equals()… menu item, and selecting any instance variables that should participate in the equality check.

Review: the StringListInterface

So last class we talked about this interface:

public interface StringListInterface {
    public void add(String s);
    public void add(int i, String s) throws IndexOutOfBoundsException;
    public String remove(int i) throws IndexOutOfBoundsException;
    public String get(int i) throws IndexOutOfBoundsException;
    public int size();
}

and wrote an array-based implementation for it.

Today I’m going to spend a little more time on an alternate implementation for StringListInterface. This will be especially useful for those of you intending to go on to 187, but it’s important regardless, since in some sense seeing-is-believing that you can have two very different ways of doing something that obey the same contract (but that might differ in, say, run-time). And it will give some context to the box-and-arrow diagrams that we’ll be using later in the class to explain why various data structures work the way they do.

First, let’s write a short, silly program demonstrating how our current code works. Let’s write a method to print out the contents of a StringListInterface:

public static void print(StringListInterface sli) {
  for (int i = 0; i < sli.size(); i++) {
      System.out.print(sli.get(i) + " ");
  }
  System.out.println();
}

Notice this is in terms of StringListInterface: it knows nothing about the ArrayStringList and only depends upon methods in the interface.

Next, a silly test method:

public static void main(String[] args) {
    StringListInterface sli = new StringArrayList();

    sli.add("Marc");
    sli.add("not");
    sli.add("so good.");
    print(sli);

    sli.add(1, "is");
    print(sli);

    sli.remove(2);
    print(sli);
}

OK, so our StringListInterface works. What about if we build it a different way?

Lions, tigers, bears, and linked lists

Remember, we built our ArrayStringList using an array as our underlying container. We hid this detail from users of the class, so it’s entirely possible we could have used a different way to build the list. Java’s builtins (that is, language constructs not library classes or methods) that let you “connect” or collect multiple variables are limited to arrays and references, but it turns out — not coincidentally — that these are all you need to build basically every other data structure. We (definitely!) won’t do them all, but we will do one more today: the linked-list. You’ll also benefit from this when we talk about how some other data structures are built later (though again, not in this level of detail, except for very simple structures).

Arrays are a fixed-size container of a sequence of cells; linked lists circumvent this restriction by making the cells (or “nodes” as they’re called in linked lists) explicit objects rather than an implicit property of an array. Here’s a simple container for Strings:

public class StringNode {
    private final String contents;

    public StringNode(String contents) {
            this.contents = contents;
    }

    public String getContents() {
            return contents;
    }
}

(On board). A simple object to hold a String and a method to get it. So far so good, except there’s no concept here of more than one StringNode being part of the same group of StringNodes. What do we do to fix this? Add a reference to a next StringNode to the current one!

public class StringNode {
    private final String contents;
    private StringNode next;

    public StringNode(String contents) {
            this.contents = contents;
            next = null;
    }

    public String getContents() {
            return contents;
    }

    public StringNode getNext() {
            return next;
    }

    public void setNext(StringNode n) {
            next = n;
    }
}

(Again on board) you can see if we want to add a node to the end of the list, it’s pretty straightforward. Inserting into the middle (or removing) is a little more complicated, but we’ll get there in a bit.

Let’s use this StringNode to build an alternate implementation of StringListInterface.

In-class exercise

But first: clicker questions (Yay?)

StringNode head = new StringNode("x");
head.setNext(new StringNode("y"));

Starting from head, what are the contents of this list?

StringNode head = new StringNode("x");
head.setNext(new StringNode("y"));
StringNode tmp = head;
head = new StringNode("z");
head.setNext(tmp);

Starting from head, what are the contents of this list?

a. “x”, “y”, “z” b. “y”, “z”, “x” c. “z” d. “z”, “x”, “y” e. “y”, “x”, “z”

Building StringLinkedList

We’ll need to keep a reference to the underlying list of nodes as an instance variable, and it turns out that’s the only instance variable we need. By convention, this is called head, and it’s initialized to null (an empty list) when we start:

private StringNode head;

public StringLinkedList() {
    head = null;
}

Let’s add the “simple” methods from last lecture, starting with size. What do we need to do? There’s no underlying array, so we can’t just examine the length attribute and return it. Instead, we need to traverse the list. In other words, (on board) we will start at the head element (if it exists), and follow the next references until we reach the end, signaled by a null value. Don’t forget that head is null, so your code should account for this:

public int size() {
    int size = 0;
    StringNode current = head;
    while (current != null) {
            size++;
            current = current.getNext();
    }
    return size;
}

Unlike arrays, where we can just jump to the node we want, a linked-list (almost) always requires that we traverse its elements until we get to the one we want.

Note that we don’t have to traverse the whole list for size; we could maintain a private size instance variable and just return it instead if we wanted to (and then, just like in StringArrayList, we need to remember to updated whenever adding or removing.) Let’s do so instead of the above:

    int size;
    StringNode head;

    public StringLinkedList() {
        size = 0;
        head = null;
    }

    @Override
    public int size() {
        return size;
    }

Now let’s do get. The exceptions are the same as before, but now we must traverse the list to find the ith element:

public String get(int i) throws IndexOutOfBoundsException {
    if (i < 0 || i >= size) {
        throw new IndexOutOfBoundsException();
    }
    int j = 0;
    StringNode current = head;
    while (current != null) {
        if (i == j) {
            return current.getContents();
        }
        current = current.getNext();
        j++;
    }
}

Now let’s look at add. If we want to add at the end of the list we can again traverse it to get to the end, and adjust the last element’s next reference appropriately. Note that we have to stop our traversal just before we get to the end, not after we go past it, which also means we have to special-case the head (on board first):

public void add(String s) {
    size++;
    StringNode node = new StringNode(s);

    // empty list? just set head to this new node
    if (head == null) {
        head = node;
        return;
    }

    // otherwise traverse to end, then append
    StringNode current = head;
    while (current.getNext() != null) {
        current = current.getNext();
    }
    current.setNext(node);
}

Similarly, if we want to add somewhere in the middle of the list, we have to stop just before we get there, and do surgery on both the previous and current (added) item to make the links in the list line up, again with a special case for the first spot:

@Override
public void add(int i, String s) throws IndexOutOfBoundsException {
    if (i < 0 || i >= size) {
        throw new IndexOutOfBoundsException();
    }

    size++;
    StringNode node = new StringNode(s);

    // adding to the front of the list
    if (i == 0) {
        // make the current head follow this node
         node.setNext(head);
         // then make this node the new head
         head = node;
         return;
    }


    // otherwise, traverse the list to find the *node-before* the new node
    // we want to insert
    StringNode nodeBefore = head;
    for (int j = 0; j < i-1; j++) {
         nodeBefore = nodeBefore.getNext();
    }

    // set the next node of the new node to the node-before's next node
    node.setNext(nodeBefore.getNext());

    // then set the node-before's node to the new node
    nodeBefore.setNext(node);
}

We’ll get to the remove method Thursday.