CPSC 120 Lecture Notes, Wednesday, Jan 28, 1998

ASSIGNMENTS/ANNOUNCEMENTS

LINKED LISTS AND POINTERS

Linked lists are most useful for situations in which the size and shape of data structures cannot be predicted in advance.

Linked lists are an example of dynamic data structures -- their size, and even shape, can change during execution.

Separate blocks of storage are linked together using pointers.

Linked representations are an important alternative to sequential representations (arrays).

Many of the key abstract data types (lists, stacks, queues, sets, trees, tables) can be represented with either linked structures or with arrays. It's important to understand the performance tradeoffs in the choice of representation.

Java does not have explicit pointers. However, it essentially has pointers in the reference data types. Recall the distinction between primitive types (int, bool, double, char, etc.) and reference, or object, types in Java.

Memory Diagrams: Some examples involving the Pet class you developed last time.

int year = 1990;
dog yourPet = new Dog("Spot", year);
dog myPet = new Dog("Rover", 1995);
year = 1980;
myPet = yourPet;
Draw the memory diagrams (called "pointer diagraming notation" in Standish.) It's important that you understand what's going on, and can represent it in your diagrams. Notice that the object corresponding to Rover is now garbage -- there is no way to get to it. Its space will be reclaimed eventually by the Java runtime system (the "garbage collector").

LINEAR LINKED LISTS

We'll start with the simplest kind of linked structure, called a linear linked list.

The list consists of a series of nodes, that is, storage blocks. Each node has a link component which points to the next node in the list. In Java, each node will be an object of some class C, and each object has an instance variable which is (a pointer to) an object of class C.

For example suppose we have a class called Book, which has instance variables for the title and the number of pages in the book.

class Book {
    String title;
    int numPages;
}
Now suppose we'd like to have a linked list of books. We can modify the Book class so that each Book object contains a pointer to another book (the next one in a list): We might want to change the name of the class to BookNode to indicate that we have made provisions for using the objects in a linked list.
class BookNode {
    String title;
    int numPages;
    BookNode link;	// pointer to next book in linked list
}
Note that the type of the link variable is the same as the class being defined.

We now have a class that represents each individual element of a list. Next we need a class that represents the list itself. What should it consist of? Some ideas:

Note that it does NOT explicitly contain all the nodes of the list -- they are part of this class indirectly, due to pointers.
class BookList {
    BookNode first;
}
What should be the operations on a linked list? Some ideas:
class BookList {
    BookNode first;
    BookList() {	// constructor
	first = null;	// points to nothing, empty list
    }
    void insertAtBeginning (BookNode newBook) {
	...
    }
}
Then you could have some code that uses the BookList class, like this:
BookList myBooks = new BookList();
BookNode bk = new BookNode("On To C",200);
myBooks.insertAtBeginning(bk);
Notice that you define TWO classes, one class for the list elements and another class for the list itself.

Space complexity: For a linked list containing n nodes, each of size s, the total amount of space required is O(n*s).

INSERTING AT THE BEGINNING OF A LINKED LIST

How do we actually insert a node at the beginning of a list? Let's start by drawing some diagrams:





Now let's describe in pseudocode what must be done:
1. make the new node's link point to the beginning of the list
2. indicate that the new node is the first node in the list
In Java: (assuming the parameter is not null)
newBook.link = first;
first = newBook;
What happens if we do these in the opposite order?
first = newBook;
newBook.link = first;
We get a cycle, and the old list is LOST! Be sure you don't lose access to your data!

Time Complexity: What is the running time of this method? O(1), because we do a constant amount of work, no matter how many nodes are in the list.

INSERTING AT THE END OF A LINKED LIST

Let's try a slightly trickier example. Suppose we want to insert a new node at the end of a list.

First, let's assume the list is empty. (How do you tell? Test if first equals null.) In this case, set the new node's link to null and set first to the new node.

Now, let's assume the list is not empty, i.e., first is not equal to null. Diagrams:






In pseudocode:
1. find the last node, n, of the list.
2. set n.link to the new node.
How do we do step 1? Search through the list, starting with first, and following link pointers, until reaching the last node (i.e., the node whose link is null).

Putting all the pieces together, we get:

void insertAtEnd(BookListNode newNode) {
    if (first == null) {
	first = newNode;
	newNode.link = null;
    }
    else {
	BookListNode node = first;	// start searching at beginning
	while ( node.link != null )
	    node = node.link;
	// now node is the last node in the list -- insert newNode
	node.link = newNode;
	newNode.link = null;
    }
}

Time Complexity: What is the running time of this operation? Everything except the while loop takes a constant amount of time. Each iteration of the while loop takes a constant amount of time. There are n iterations of the while loop. So we have O(n*1 + 1) = O(n).

How could we improve this running time? We could a constant amount of extra information in the linked list, a pointer to the last node, as well as the first node. Then we would just have to do:

last.link = newNode;
last = newNode;
newNode.link = null;
So it would be constant time.

DELETING IN A LINKED LIST

Let's look at an even more involved example. This one is to delete the node at the end of the list. First, we have to find the last element of the list. We can do that similarly to the way we found the end of the list in the previous example. But then to remove it from the list, we have to change the link of the preceding node to null. In pictures:




How do we know which node is the preceding one? The links are one-directional; they don't go backwards.

The solution is to do some extra work while we are traversing the list. Instead of just keeping track of the current node, also keep track of the previous node.

First, let's handle the special cases (also called boundary conditions), when the list is empty and when it has only one element.

If the list is empty, then nothing needs to be done.

If the list has only one element, then we just need to set first to null. How do we tell if the list has only one element? We check whether first.link is null.

Now let's consider the meat of the problem. The list contains at least two nodes.

1. continue marching down the list with the two pointers, curNode
   and prevNode, until curNode is the last node in the list.
2. set prevNode.link to null
How do we do step 1?
1.1 initialize prevNode to the first node and curNode to the second node
1.2 while curNode.link is not null
1.3    set curNode to curNode.link
1.4    set prevNode to prevNode.link
Putting all the pieces together:
void deleteLast() {
    if (first == null) return;	// empty list
    if (first.link == null) {	// one-node list
	first = null;
	return;
    }
    // list has at least two nodes
    BookListNode prevNode = first;
    BookListNode curNode = first.link;
    while ( curNode.link != null ) {
	curNode = curNode.link;
	prevNode = prevNode.link;
    }
    // now curNode is last node in list and prevNode is second-to-last
    prevNode.link = null;
}

Time Complexity: The running time here, like the last example, is O(n). Would it help to keep a last pointer? No! We still can't follow the pointer backward.

OTHER LINKED STRUCTURES

We don't have to restrict ourselves to just having one link instance variable. We can get arbitrarily complicated linked structures. Some of the more common and useful ones are:

SUGGESTIONS AND WARNINGS

Be sure to check that a link is not null before following it! For instance, the following is an error:
node.link = null;
node = node.link;	// now node equals null
node.link = null;	// ERROR!  since node is null, it has no link field
To avoid this, be sure to check for null pointers in situations where it is possible that they exist.

Mark end of list by setting the link field of the last node to null.

Be careful with boundary cases! Examples of boundary cases are the empty list, the list with one element (in some cases), the first node, the last node, etc. (depending on what you are doing).

Draw memory diagrams! These can usually make it clear what you need to do, and in what order.

Don't lose access to needed objects! Make sure you change pointers in a safe order. (See example above.)