07: Linked Lists
Announcements
Please use Piazza responsibly: mark things as resolved (or not) when they’re done or when you have new follow-up questions or answers. Don’t put non-answers in the “student answer” box; use the “followup discussion” for comments and the like. Set “resolved” (or unset it!) appropriately, as we can’t read and re-read each question so we use the “resolved” status to check for things that are done or still need attention.
Quiz makeups: Generally all makeups must be completed within a week of a quiz. We’ll make an exception for the first quiz (snow, etc.) 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.
There was a minor error in the notes on remove()
from last class, it’s since been corrected (in short, an array boundary was violated, oops!).
Check your grades in Moodle please!
Assignment 04 guidance
A04 requires that your code be able to compare nodes 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 String
s:
public class StringNode {
private final String contents;
public StringNode(String contents) {
this.contents = contents;
}
public String getValue() {
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 StringNode
s. 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 getValue() {
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
.
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 i
th 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.getValue();
}
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.