ICS 46 Spring 2022
Notes and Examples: Linked List Variations


Linked lists, revisited

In your previous coursework (such as ICS 33 or an equivalent course elsewhere), you've likely been introduced to linked data structures, the simplest of which is a one-dimensional sequence of elements called a linked list. If, on the other hand, you've never heard of linked lists before, it might be worth brushing up on this prerequisite topic. One way to get started would be to read this set of background notes written for my ICS 45C course, which covers this topic from a C++ implementation perspective, though the analysis on this ICS 46 page isn't particularly focused only on C++.


Asymptotic analysis of a few linked list variations

While the concept of a linked list is straightforward, it's worth noting that there are a few variations on the concept. None is indisputably better than the others; instead, they each present different tradeoffs, with some using additional memory and complexity in order to guarantee that certain operations run faster. So when we want to implement a linked list, we have a decision to make: Which variation should we implement? In order to answer that question, we first have to understand what the tradeoffs are; it's time to do asymptotic analysis on each variation, so we can understand the situations in which each one excels.

Singly-linked list with head

The simplest variation of linked list is what you might call a singly-linked list with head. Its name implies its two most important qualities:

A singly-linked list with head looks something like this:

Singly-Linked List with Head (Diagram)

Now, let's do some asymptotic analysis of a set of operations you might like to perform on a singly-linked list with head, so we can see where it excels and where it falters. We'll assume that the list contains n elements at the time the operation is performed.

We see, generally, that the head pointer can allow us to very quickly manipulate the front of the list, but that it is much more expensive to manipulate the end. One way to solve that problem might be to keep track of where the end is at all times.

Singly-linked list with head and tail

A singly-linked list with head and tail is similar to a singly-linked list with head, and its name again focuses on its most significant qualities:

Pictorially, a singly-linked list with head and tail looks like this:

Singly-Linked List with Head and Tail (Diagram)

To compare this variation to the singly-linked list with head, we can analyze the same set of operations we did before, and see which ones (if any) got better and which (if any) got worse.

So, all in all, not very much changed. One operation — adding to the end of the list — got substantially better, and nothing really got any worse. But it's a surprisingly small benefit for what seemed like it might be a really worthwhile change. And, in truth, the change is worthwhile if you know that adding to the end is one of the key operations you need. But, generally, this leaves us feeling a bit underwhelmed. And it should be noted that we gave something up in the deal: Some of the constant-time operations got a bit slower (because of the need to manipulate the tail pointer) and a bit more complex (for the same reason).

In general, we wouldn't expect anything to really help with the Get the ith element in the list operation. A fundamental quality of a linked list is that you have to walk from one node to the next, because there's no rhyme or reason to where the nodes are located in memory; just because you know you want the 10th node doesn't give you any insight into where it will be. But, at the very least, it seems like the Remove an element from the end of the list operation could be better. And there's one important tweak that can get us there.

Doubly-linked list with head and tail

A doubly-linked list with head and tail is a slight rethinking of what we've seen so far; in particular, it adds the notion of a second link between nodes.

We might draw a doubly-linked list with head and tail this way:

Doubly-Linked List with Head and Tail (Diagram)

Once again, we'll do an asymptotic analysis of the same set of operations, so we can understand what this new idea buys us (if anything).

It's important to note that, while we got some benefit, we did make a couple of addditional tradeoffs. We've spent some extra memory here, because every node is going to need to store an additional pointer; that may or may not be significant, depending on how much memory is available to us, but is something to consider. We've also made the various constant-time operations a little bit slower, because there's an additional pointer to check or manipulate. And, finally, we've made our linked list implementation even more complex than it was before.

Choosing the appropriate variation, in practice, is going to be a matter of understanding the actual problem we're trying to solve, determining which operations we're going to need in order to solve it, and selecting the simplest variation that performs well enough in the ways that we need it to. We'll see a few practical examples of this soon.

Circular variants

For each of the above variants of linked lists, another option involves what you might call circularity. A circular linked list is one in which the end connects back to the beginning — and, in the case of a doubly-linked circular list, the beginning connects back to the end. In other words, in a circular linked list, the next pointer in the last node points to the first node (i.e., the same node the head pointer points to); if there is a previous pointer in each node, the previous pointer of the first node points to the last node.

This doesn't make an appreciable difference in the analyses we did above, but does allow some lower-level implementation gains, such as the ability to never go "too far" in a singly-linked list (i.e., starting from anywhere, you can get anywhere) and the ability to traverse the same list multiple times. Those can be small-time wins, or they can form the basis of a superior implementation for a more complex data structure; more on that later.