Lecture 07: Intro to Linked Lists

Welcome

Special Guest from iCons

See Piazza post for details.

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!

I have to leave right after class today. I apologize, as I will not be able to answer questions as usual.

Assignment 04 reminder

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

Recall we were talking about enlarge. 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

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

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

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, time or space efficiency). 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: more clicker questions

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?