Special Linked Lists Introduction (to Special Linked Lists): In this lecture we will cover some standard variations of simple linear-linked lists: circular, header, trailer, and doubly-linked lists. We will discuss the tradeoffs between using these lists and simple linear-linked lists. Circular Lists: Standard linearly-linked list are used more often than any of these variants, but it is interesting to see what tradeoffs the variants allow. The web reading discusses these special lists in more detail. It would be an excellent idea to hand simulate the methods there to become more familiar with how these special lists are processed. In "circular lists", the "rear" node doesn't store null in its .next field, but instead has its .next field refer to the "front" node in the list. I put "rear" and "front" in quotes because in a circular list, there really is not an obvious front or rear node (unless we have a special reference into the list named front or rear: we need to refer to some node in the linked list, to be able to iterate over its values. There are a few applications for circular lists. One allows us to represent and efficiently process a queue by having a single reference to the "rear" value in the queue (instead of one to the front and one to the rear). Note that in a circular list, the node one beyond the rear is the front, so both the front and rear nodes in a queue are easily reached from the rear node (and all "action" in a queue is at the front/remove or end/add). If we referred to the front node, there could be an arbitrary number of nodes between it and the rear node. If rear refers to the last node an a queue represented by a circular list, here is code to add to the rear of the queue. Draw a picture of an empty list, a list with one node, and a list with 3-5 nodes and simulate this code on those lists to see that it is correct. if (rear != null) rear = rear.next = new LN(someValue,rear.next); else rear = new LN(someValue,null); rear.next = rear; //Make it a 1 node circular list } If rear refers to the last node an a queue represented by a circular list, here is code to remove the front of the queue. Draw a picture of an empty list, a list with one node, and a list with 3-5 nodes and simulate this code on those lists to see that it is correct. if (rear != null) { int frontValue = rear.next.value; //Front is after rear if (rear.next == rear) //Just 1 node in the list? rear = null; else rear.next = rear.next.next; //rear.next refers to new front } If we had to represent a huge number of queues, most of which are empty, using a class that stored one reference to the rear of a circular list will save space when compared to using a class that stores two references to the front and rear of a linear-linked list. Header Lists: In "header lists", every list has a special (valueless) header node at the front. So, an "emtpy header list" would have one list node in it. All "real" nodes in the list come after the header. By using a header node, special cases in the code are reduced. For example, I wrote the following code for adding a value to the end of a linked list, in a class with front and rear instance variables. if (front == null) front = rear = new ListNode(e,null); else rear = rear.next = new ListNode(e,null); If the linked list were a header list, if it were empty the front and rear would both be set to the header node (whose .next is set to null in the constructor). We could remove the special case above (front is never null) and simplify the code to the if-less rear = rear.next = new ListNode(e,null); In a header list, one never changes front: it always refers to the header. So every node we manipulate in the list is guaranteed to have a previous node referring to it (via its .next field). This guarantee often simplifies the code for adding and removing nodes, although for other operations (like traversal) we must remember to skip the header node. Trailer Lists: In "trailer lists", every list has a special (valueless) trailer node at the end. So, an "emtpy trailer list" would have one list node in it. All "real" nodes in the list come before the trailer. By using a trailer node, we can remove a node that we have a reference to, without having a reference to the node before it! I'll show this "trick" in class; hand simulate the following code again to see how it works (do it with r referring to the first, last and middle node in a trailer list). Note that the "last" node IN a trailer list is the one before the trailer node. This code works correctly only if the node r refers to is always followed by another node (which is guaranteed for a trailer list; and r -the node to be removed- must never refer to the trailer itself). r.value = r.next.value; r.next = r.next.next; Doubly-Linked Lists: In "doubly linked lists", each node refers to the one after it (.next) and the one before it (.prev). So, we can traverse the list in both directions: we can reach any node from any other node. The cost is an increase of space needed to store a doubly-linked list: twice as many references in each node (2 instead of 1) and the requirement to change twice as many references when we mutate the list, e.g., when adding/removing values. For example, when we add a node to a doubly linked list, we need to make the .next of the one before it refer to the new node, and the .prev of the one after it refer to the new node. And in the new node itself, we need to set its .next as well as its .prev instance variables. If the node added is at the front or rear of a doubly linked list, there are all sorts of special cases to handle. By having both a header and a trailer node in a doubly linked list, we can simplify this code and remove all the special cases. But now, even an empty list has two nodes: the header and trailer.