These notes are an abridged, concatenated, and somewhat revised version of Jonathan Shewchuk's 61B Spring 2014 notes. The originals can be found at: http://www.cs.berkeley.edu/~jrs/61b/lec/07 and http://www.cs.berkeley.edu/~jrs/61b/lec/08 LINKED LISTS (a recursive data type, we saw this section last week) ============ Last week, we discussed the idea of a Scheme-like list in Java which we called a linked list. A linked list is made up of _nodes_. Each node has two components: an value, and a reference to the tail node in the list. These components are analogous to "car" and "cdr" in Scheme. However, our node is an explicitly defined object. public class IntList { // IntList is a recursive type public int value; public IntList tail; // Here we're using IntList before } // we've finished declaring it. Let's make some IntLists. IntList l1 = new IntList(), l2 = new IntList(), l3 = new IntList(); l1.value = 7; l2.value = 0; l3.value = 6; ------------- ------------- ------------- | ----- | | ----- | | ----- | |value| 7 | | |value| 0 | | |value| 6 | | l1-->| ----- | l2-->| ----- | l3-->| ----- | | ----- | | ----- | | ----- | | tail| ? | | | tail| ? | | | tail| ? | | | ----- | | ----- | | ----- | ------------- ------------- ------------- Now let's link them together. l1.tail = l2; l2.tail = l3; What about the last node? We need a reference that doesn't reference anything. In Java, this is called "null". l3.tail = null; ------------- ------------- ------------- | ----- | | ----- | | ----- | |value| 7 | | |value| 0 | | |value| 6 | | l1-->| ----- | l2-->| ----- | l3-->| ----- | | ----- | | ----- | | ----- | | tail| .-+-+-------->| tail| .-+-+-------->| tail| X | | | ----- | | ----- | | ----- | ------------- ------------- ------------- To simplify programming, let's add some constructors to the IntList class. public IntList(int i, IntList n) { value = i; tail = n; } public IntList(int i) { this(i, null); } These constructors allow us to emulate Scheme's "cons" operation. IntList l1 = new IntList(7, new IntList(0, new IntList(6))); Linked list insertion ---------------------------- Unlike arrays, linked lists can keep growing until memory runs out. The following method inserts a new value into the list immediately after "this". public void insertAfter(int value) { tail = new IntList(value, tail); } l2.insertAfter(3); ------------- ------------- ------------- ------------- | ----- | | ----- | | ----- | | ----- | |value| 7 | | |value| 0 | | |value| 3 | | |value| 6 | | l1-->| ----- | l2-->| ----- | | ----- | l3-->| ----- | | ----- | | ----- | | ----- | | ----- | | tail| .-+-+------>| tail| .-+-+-->| tail| .-+-+------>| tail| X | | | ----- | | ----- | | ----- | | ----- | ------------- ------------- ------------- ------------- A List Class (new stuff) ------------ There are two problems with the IntList idea. (1) Suppose x and y are pointers to the same shopping list. Suppose we insert a new value at the beginning of a String version of IntList thusly: y = x; x = new StringList("soap", x); y doesn't point to the new value; y still points to the second value in x's list. If y goes shopping for x, he'll forget to buy soap. Gross. (2) How do you represent an empty list? The obvious way is "x = null". However, Java won't let you call an IntList method--or any method--on a null object. If you write "x.insertAfter(value)" when x is null, you'll get a run-time error, even though x is declared to be a IntList. (There are good reasons for this, which you'll learn later in the course.) For these two reasons, the name 'IntList' is arguably not a very good one. It just doesn't quite behave how our intuition would suggest. For that reason, we'll do two things: I. First, we'll define a new class called 'SListNode' as follows: public class SListNode { public int item; public IntList next; } Note that an SListNode is structually EXACTLY the same as an IntList. The only differences are the class name and the field name difference. However, we'll conceptualize how this class is used in a completely different way. II. Second, we'll create a separate List class, whose job is to maintain the head (first node) of the list. We will put many of the methods that operate on lists in the SList class, rather than the SlistNode class. public class SList { private SlistNode head; // First node in list. private int size; // Number of items in list. public SList() { // Here's how to represent an empty list. head = null; size = 0; } public void insertFront(Object item) { head = new SlistNode(item, head); size++; } } SList object SlistNode object ------------- ------------- String object ----- | ----- | | ----- | ---------- x | .-+----->| size| 1 | | | item| .-+-+---->| milk | ----- | ----- | | ----- | ---------- ----- | ----- | | ----- | y | .-+----->| head| .-+-+-------------------->| next| X | | ----- | ----- | | ----- | ------------- ------------- Now, when you call x.insertFront("fish"), every reference to that SList can see the change. SList SlistNode SlistNode ------------- ------------- ------------- ----- | ----- | | ----- | -------- | ----- | -------- x | .-+-->| size| 2 | | | item| .-+-+->| fish | | item| .-+-+->| milk | ----- | ----- | | ----- | -------- | ----- | -------- ----- | ----- | | ----- | | ----- | y | .-+-->| head| .-+-+-->| next| .-+-+----------->| next| X | | ----- | ----- | | ----- | | ----- | ------------- ------------- ------------- Now y will never forget to buy fish, soap, milk, or anything else at all. Another advantage of SLists --------- Another advantage of the SList class is that it can keep a record of the SList's size (number of SlistNodes). Hence, the size can be determined more quickly than if the SlistNodes had to be counted. Today, I want to introduce another advantage of the SList class. We want the SList ADT to enforce two invariants: (1) An SList's "size" variable is always correct. (2) A list is never circularly linked; there is always a tail node whose "next" reference is null. Both these goals are accomplished by making sure that _only_ the methods of the SList class can change the lists' internal data structures. SList ensures this by two means: (1) The fields of the SList class (head and size) are declared "private". (2) No method of SList returns an SlistNode. The first rule is necessary so that the evil tamperer can't change the fields and corrupt the SList or violate invariant (1). The second rule prevents the evil tamperer from changing list items, truncating a list, or creating a cycle in a list, thereby violating invariant (2). DOUBLY-LINKED LISTS =================== As we saw last class, inserting an item at the front of a linked list is easy. Deleting from the front of a list is also easy. However, inserting or deleting an item at the end of a list entails a search through the entire list, which might take a long time. (Inserting at the end is easy if you have a `tail' pointer, as you will learn in Lab 3, but deleting is still hard.) A doubly-linked list is a list in which each node has a reference to the previous node, as well as the next node. class DListNode { | class DList { Object item; | private DListNode head; DListNode next; | private DListNode tail; DListNode prev; | } } | ------------- ------------- ------------- | item| | item| | item| head | -----| | -----| | -----| tail ----- |----- | 4 || |----- | 1 || |----- | 8 || ----- | . +->|| X | -----|<-----++-. | -----|<-----++-. | -----|<-+-. | ----- |----- -----| |----- -----| |----- -----| ----- |prev | .-++----->|prev | .-++----->|prev | X || | -----| | -----| | -----| | next| | next| | next| ------------- ------------- ------------- DLists make it possible to insert and delete items at both ends of the list, taking constant running time per insertion and deletion. The following code removes the tail node (in constant time) if there are at least two items in the DList. tail.prev.next = null; tail = tail.prev; You'll need a special case for a DList with no items. You'll also need a special case for a DList with one item, because tail.prev.next does not exist. (Instead, head needs to be changed.) Let's look at a clever trick for reducing the number of special cases, thereby simplifying our DList code. We designate one DListNode as a _sentinel_, a special node that does not represent an item. Our list representation will be circularly linked, and the sentinel will represent both the head and the tail of the list. Our DList class no longer needs a tail pointer, and the head pointer points to the sentinel. class DList { private DListNode head; private int size; } sentinel ------------- ----- | item|<---+-. | --------------->| -----| ----- | |prev | X || head | |----- -----| | || .-+------+----------------- | |----- -----| | | ---------+------+-. || | | | | next-----|<---------------+----- | | ------------- | | | v v | ---+--------- ------------- ------------- | | | item| | item| | item| | | | -----| | -----| | -----| | |--+-- | 4 || |----- | 1 || |----- | 8 || | || . | -----|<-----++-. | -----|<-----++-. | -----| | |----- -----| |----- -----| |----- -----| | |prev | .-++----->|prev | .-++----->|prev | .-++--- | -----| | -----| | -----| | next| | next| | next| ------------- ------------- ------------- The invariants of the DList ADT are more complicated than the SList invariants. The following invariants apply to the DList with a sentinel. (1) For any DList d, d.head != null. (There's always a sentinel.) (2) For any DListNode x, x.next != null. (3) For any DListNode x, x.prev != null. (4) For any DListNode x, if x.next == y, then y.prev == x. (5) For any DListNode x, if x.prev == y, then y.next == x. (6) A DList's "size" variable is the number of DListNodes, NOT COUNTING the sentinel (denoted by "head"), that can be accessed from the sentinel by a sequence of "next" references. An empty DList is represented by having the sentinel's prev and next fields point to itself. Here's an example of a method that removes the last item from a DList. public void removeBack() { if (head.prev != head) { // Do nothing if the DList is empty. head.prev = head.prev.prev; // Sentinel now points to second-last item. head.prev.next = head; // Second-last item now points to sentinel. size--; } }