# Outline of Class 27: Introduction to Lists

Held: Monday, March 9, 1998

## Administrivia

• I've graded the first exam, but won't be returning them today because the snow prevented me from getting an answer key done.
• It appears that a draft of the test overwrote the actual test. Question three was undoable (more or less), so I gave you all full credit. Feel free to let me know if there are other ameliorative measures you think I should take.
• Today is pretty close to a snow day. However, it seems unfair to those who made it in to cancel class. Those who missed class should carefully read the notes and chat with your colleagues.
• I hope to have a lab ready for Wednesday that will combine sorting and lists.

## Lists

• Lists are among the simplest of data structures.
• Like arrays and vectors, they are used for storing and retrieving data.
• In effect, a list is an ordered collection of information.
• Unlike arrays and vectors, lists do not necessarily guarantee constant time access to each element.
• However, lists do (usually) guarantee constant time insertion of elements at a few places in the list.
• Some list implementations guarantee constant time insertion at the front of the list.
• Some list implementations guarantee constant time insertion at the end of the list.
• Some list implementations provide a "current element" and guarantee constant time insertion after/before the current element.
• As before, we can begin to define lists by the operations we permit. What are reasonable operations?
• Creation of new, empty, lists.
• Insertion of elements in the list.
• Deletion of elements from the list.
• Test emptiness of the list.
• Test containment of objects in the list.
• Modification of elements in the list.
• Determine size of the list.
• Extract the first element of the list. This is often referred to as the head or car of the list.
• Extract all but the first element of the list. This is often referred to as the tail or cdr of the list.
• We must also decide whether multiple lists can share structure. That is, if L is a list, can M be a list with an extra element at the front and L as the remainder and N be a list with an extra element at the front a L as the remainder? If so, does a change to the second element of M affect N?

### Implementation Issues

• There are a number of ways in which we can implement lists, and these implementations govern the cost of the various operations.
• Most programmers expect that insertion and testing for emptiness are constant time operations. Certain kinds of deletion are also constant time (e.g., delete the first element).
• There are a number of hidden issues to consider when designing implementations. For example, when we ask for the `tail` of a list and then modify that tail, do we modify the original list?
• Here's one example in Java form
```// Assume l is the list 3,6,8,32
lprime = l.tail();
lprime.addToHead(new Integer(5));
System.out.println(l.tail().head());
```
• Here's a similar example that might have different results (at least in terms of our understanding of what modifications mean)
```// Assume l is the list 3,6,8,32
lprime = l.tail();
lprime.remove(new Integer(8));
System.out.println(l.contains(new Integer(8)));
```
• In designing our implementations, we will need to consider tradeoffs between efficiency, memory usage, simplicity of implementation, and ambiguity.

### Singly Linked Lists

• There are a number of ways to implement lists, including using other data structures to implement them.
• However, the most basic implementation is the singly-linked list, in which a list is represented as a series of nodes, each of which has a single link to the next element in the list.
• As a Java class,
```/**
* Nodes for a singly-linked list.
*/
public class Node {
/** The value in the current node.  A value of `null`
* indicates (...)
*/
protected Object value;
/** The next node in the list.  A value of `null`
* indicates that this is the last element of the list.
*/
protected Node next;
...
} // Node
```
• We use a special value, null, for links from nodes that have no next element.
• Alternatively, you can think of null as representing a "non node".
• We then wrap our nodes in a separate list class. Why? So that it's easier to handle issues like insertion and removal.
• However, this can make it harder to write methods like `tail`, which now needs to create new list objects.
• Scheme lists do not have this separate wrapper class for just this reason.
• If you're using a wrapper class, you can also include a number of other fields, such as a link to the current element of the list and a link to the end of the list.
• For our purposes, we can assume the following definition
```/**
* A singly-linked list.
*/
public class List {
/** The elements of the list, starting at the front */
protected Node front;
/** The last element of the list. */
protected Node back;
/** The current element of the list. */
protected Node current;
...
} // List
```

#### Insertion

• One primitive operation on lists is insertion.
• As discussed earlier, there are a number of forms of insertion in a list, including
• Insertion of a new element at the head
• Insertion of a new element at the tail
• Insertion of a new element before/after the current position
• If we are using the `List` class, we might write our insert at head function as follows
```/** Insert a non-null object at the head of the list.
* pre: The element is not null.
* pre: The current list has been initialized
* post: The head of the list is now that element.
* post: The tail of the list is the previous list.
*/
public void insertAtHead(Object element) {
// Create a node to hold the new element.
Node new_head = new Node(element,null);
// Is the list currently empty?  If so, just insert the element
// at front and back, and we're done.
if (front == null) {
front = new_head;
back = new_head;
current = new_head;
}
// Otherwise, update the next reference of the new head
// and replace the front of the list
else {
new_head.setNext(front);
front = new_head;
// We don't need to modify the back of the list.
// Should we modify current?
}
} // insertAtHead
```
• If we are not using a separate `List` structure, we can't really talk about modifying the current list. Hence, we need to create a static method that takes a `Node` as a parameter, and returns a (potentially new) `Node`.
```/**
* Construct a new list from a new head element and old tail.
*/
public Node cons(Element head, Node tail) {
return new Node(head,tail);
} // cons
```
• Insertion at the end of the list (i.e., appending to a list) also depends on which representation of list you choose, and whether or not there is a `back` field.
• In the simplest `List`-based version (in which we do have a `back`.
```/**
* Append an element to the end of the list.
* pre: The element is not null.
* pre: The list has been appropriately initialized and maintained.
* post: The list now contains the element at the end.
*/
public void appendToEnd(Object element) {
// Create the node for the new element
Node new_end = new Node(element,null);
// Special case: the list is empty
if (front == null) {
front = new_end;
back = new_end;
current = new_end;
}
// Normal case: we just need to update the end
else {
back.setNext(new_end);
back = new_end;
}
} // appendToEnd
```
• We can make this more complicated by dropping the `back` field.
```public void appendToEnd(Object element) {
// Create the node for the new element
Node new_end = new Node(element,null);
// Special case: the list is empty
if (front == null) {
front = new_end;
}
// Normal case: need to traverse the list until we find
// the end
else {
Node traverse = front;
while (traverse.next != null) {
traverse = traverse.next;
}
traverse.next = new_end;
}
} // appendToEnd
```
• We would use a similar technique for our node-based version.

#### Deletion

• The converse of insertion is deletion. We will frequently need to delete elements from our list.
• As with most list-based functions, there are a number of varieties of deletion.
• We might delete the head of a list.
• We might delete the last element of a list.
• We might delete the current element of a list.
• We might delete all instances of a particular object.
• We might delete the first instance of a particular object.
• Our implementation of deletion also depends on how we are representing lists (with a wrapper class, or just with the `Node` class).
• Using our standard the wrapper class, deletion at the head is relatively easy: we just update the front to the thing next to the front.
```/**
* Delete the first element in the list, returning that element.
* pre: the list is nonempty
* post: the list is one element smaller and no longer
*   contains the previous head element
public Object deleteHead() throws ListException {
// Special case: the list is empty
if (front == null) {
throw new ListException("Can't delete the head of an empty list");
}
// Standard case: the list is nonempty
else {
Node deleted = front;
// Update the front
front = front.nextNode();
// Update current, if necessary. == is the appropriate comparison
// operation since we want to make sure that we're referring to
// the same node, and not just two nodes with the same contents.
if (current == deleted) {
current = front;
}
// Return the front element
return deleted.contents();
} // else
} // deleteHead
```
• Note that while I've used methods of my `Node` class, it would also be reasonable to directly access `Node`'s fields, since these are related classes, and would be in the same package.
• Head-deletion in the `Node`-based version is really just a call to `tail`. To delete the head of a list `l`, you would just use
`l = l.tail()`
• Deletion from the end of a list is more difficult, as you need to step through until are two back from the end of the list.
```/**
* Delete the last element in the list, returning that element.
* pre: the list is nonempty
* post: the list is one element smaller and no longer
*   contains the previous final element
* post: if the current element was the final element, the
*   current element is now the first element.
*/
public Object deleteLast() throws ListException {
// The element we're about to delete
Node deleted;
// Special case: the list is empty
if (front == null) {
throw new ListException("Can't delete the last element in an empty list");
}
// Special case: the list has one element
else if (front.next == null) {
deleted = front;
front = null;
current = null;
back = null;
return deleted.contents();
} // only one element
// Standard case: the list has at least two elements
else {
// The element we're currently looking at
Object pointer = front;
// The element we're about to delete
Object
// Keep going until we reach the last node
while (pointer.nextNode().nextNode() != null) {
pointer = pointer.nextNode();
} // while
// We're now one step back from the end, so delete
// the next element.
deleted = pointer.next;
pointer.next = null;
// Update the current element if necessary.
if (current == deleted) {
current = front;
}
// Return the element
return deleted.contents();
} // List of length two or more
} // deleteLast
```
• Deleting all copies of a particular element can be somewhat more difficult, since it may require the "double next" pointer strategy used above.
• However, we can simplify our implementation by using a recursive `Node`-based deletion.
• Here are the two related functions
```/**
* Delete all copies of an element from a sequence of `Nodes`.
* pre: (none)
* post: The returned list no longer contains any copies of the element.
* post: Any lists that contain sublists may also be modified.
*/
public Node deleteCopies(Node list, Object element) {
// Base case: empty list
if (list == null) {
return null;
}
// The head of the list contains the element.  Delete and
// recurse
else if (list.contents().equals(element)) {
return deleteCopies(list.tail(), element);
}
// The head of the list doesn't contain the element.  Work on
// the tail.
else {
list.setNext(deleteCopies(list.tail(), element));
return list;
}
} // deleteCopies

/**
* Delete all copies of an element from a list.
* pre: (none)
* post: The returned list no longer contains any copies of the element.
* post: Any lists that contain sublists may also be modified.
* post: If the current element was deleted, the current element
*   now refers to the front of the list.
*/
public void deleteCopies(element) {
front = deleteCopies(front,element);
// Fix current element if necessary
if (current.contents().equals(element)) current = front;
// Recompute the last element if necessary
if (back.contents().equals(element)) recomputeBack();
} // deleteCopies
```

On to Sorting Lists
Back to Sorting Without Swapping
Outlines: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
Current position in syllabus

Disclaimer Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

Source text last modified Tue Jan 12 11:52:25 1999.

This page generated on Mon Jan 25 09:49:28 1999 by SiteWeaver. Validate this page.

Contact our webmaster at rebelsky@math.grin.edu