07: Intro to Linked Lists

Welcome

Announcements

Quiz makeups: Generally all makeups must be completed within a three days of a quiz. If you miss a quiz, you need to make it up soon unless you are totally out of school due to illness or whatnot. Arrange your makeup with your TA.

Check your grades in Moodle please!

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.

I suggest you ask Eclipse to write an 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 started talking about 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();
}

Today, let’s finish it, and then we’ll move onto an alternative.

Writing the StringArrayList

(If you want to implement an interface, use the Eclipse “new class” wizard to note you want to do so, and it will write skeletons of each method for you.)

Let’s think about what we need in our implementation. What instance variables do we need? Certainly an array of Strings to hold the list elements. Anything else? The number of elements actually stored in the array. Remember, one of the reasons we’re writing a List is that arrays are of fixed size, but a List can grow and shrink arbitrarily. (We’ll see how soon.)

So let’s declare an array String[] array and an int size to hold these values.

public class StringArrayList implements StringListInterface {
    String[] array;
    int size;
}

(On board) Conceptually, strings will be added to, gotten, and removed from this array; it’s the implementation’s job to make sure they go in the right place, and that if a user of StringArrayList tries to access an element of the List (that is, of the underlying array) that is invalid, an exception is raised.

Let’s start with a constructor that starts with a small empty array:

public StringArrayList() {
  array = new String[10];
  size = 0;
}

Now let’s do the simple methods:

public int size() {
  return size;
}


@Override
public String get(int i) throws IndexOutOfBoundsException {
  return array[i];
}

But remember, while the array might be of size 10, there might be fewer than 10 (even no!) strings stored in the array. So a correct definition would instead read:

public String get(int i) throws IndexOutOfBoundsException {
  if (i >= size or i < 0) {
    throw new IndexOutOfBoundsException();
  }

  return array[i];
}

This is important to understand: the List acts like a list of elements with a size equal to the number that have been added (less the number removed). Even though there’s an underlying array of a different size, the user of the List interface cannot see it! The details are said to be encapsulated. This is a very powerful concept that lets you use data structures (and generally any API) by reading their contract – you don’t need to fully understand every detail of the implementation (though it can be helpful to do so!).

Now let’s turn to some of the more complicated methods, like add:

public void add(String s) {
  array[size] = s;
  size++;
}

This sorta works, but what happens once we add the eleventh element? We’ll overflow the array bounds, which we don’t want to do – our list is supposed to be unbounded. Instead, we’ll check to see if the array needs to be expanded, and do so:

public void add(String s) {
  if (size == array.length) {
    enlarge();
  }
  array[size] = s;
  size++;
}

In-class exercise

public void add(String s) {
  if (size == array.length) {
    enlarge();
  }
  size++;
  array[size] = s;
}

What should enlarge do? It should allocate a new, larger array, copy the current array into it, then set the strings instance variable to point to this new array.

void enlarge() {
  String[] larger = new String[array.length * 2];
  for (int i = 0; i < array.length; i++) {
    larger[i] = array[i];
  }
  array = larger;
}

Why double, and not, say, just + 10? The full answer is beyond the scope of this course, but in short: when you don’t know anything else, doubling is the most efficient way to dynamically grow an array. If you do know other things, you might expose ways to grow (or shrink) the underlying array, but that’s has its own problems (like: now users of your code are tied to your specific implementation, even if a better one comes along later).

What about if we want to add in a particular place, rather than just at the end of the array? We need to move each element out of the way. (On board) we have to move the last element forward one, then the previous element into the last element’s place, and so on, to “make space” for the item we’re inserting. We also need to make sure the index is valid, and that there’s space. In code:

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

  if (size == array.length) {
    enlarge();
  }
  for (int j = size; j > i; j--) {
    array[j] = array[j-1];
  }
  array[i] = size;
  size++;
}

Notice that we never worry about “checking” what’s already in an array cell. If it’s of index size, then it’s not in use right now.

Finally, let’s write the code to remove an element at index i. Similar to the above, we’ll need to “move” any elements into the space we leave behind (on board). And by convention, return the value we removed.

public String remove(int i) throws IndexOutOfBoundsException {
  final String removed = strings[i];
  if (i >= size || i < 0) {
    throw new IndexOutOfBoundsException();
  }

  for (int j = i; j < size - 1 ; j++) {
    array[j] = array[j+1];
  }

  // optional
  array[size-1] = null;

  size--;
  return removed;
}

Setting the unused space to null lets the garbage collector free the memory, but that reference will also be overwritten the next time we add to the list, so it’s not strictly necessary.

As I mentioned earlier, there are many other things you could do. For example, you could write removeAll method that completely empties the List.

At least two solutions are possible.

In-class exercise

One is to repeatedly call remove() until the list is empty; another is to directly manipulate the underlying instance variables.

public void removeAll1() {
  while (size() > 0) {
    remove(0);
  }
}

public void removeAll2() {
  size = 0;
  array = new String[10]; // this line is optional; do you see why?
}

Next! Linked lists!

Next, we’re 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”

We’ll pick up here next class.