A LinkedList is a data structure that organizes the data contained within it by making each location, called a node, responsible for holding references to its immediate neighbors in the list (along with the actual data we wish to store in the list). It's a fascinating data structure, but before we get too far, it's time for a quiz.
What is similar about the following images?

If you answered that each one is balancing an object on their head, you are half way to understanding the linked list. Just for fun, we'll call each person in the photos above a node. A node is something that can keep track of an object (by balancing it on its head, in this case).
But nodes can do more than just keep track of an object. Consider the following nodes...

Each node here is holding the hand of their two closest neighbors. This is cool, but not nearly as cool as it would be if each of them was balancing an object on their head. Shut your eyes and see if you can you imagine it with objects appearing on their heads? If so, you've just realized two things: 1) pretty much how a linked list works and 2) that it really is cool.
The LinkedList itself just manages how to get to the front of the chain of nodes and the back of the chain of nodes. Essentially, the LinkedList contains two attributes: head — a reference to the node at the beginning of the list and tail — a reference to the node at the end of the list.
In Pictures
Our LinkedList keeps track of the first (or head) node and the last (or tail) node. Each node is responsible for holding hands with its neighbors. Of course, if you don't have a neighbor, leaving your hand dangling in the air just looks stupid, so our stick figures put their hand(s) in their pocket(s) to signify that they don't have a neighbor.
A LinkedList containing one star consists of a reference to the head and tail (which happen to be the same thing) and one node balancing a star on its head and both hands in its pockets.

A LinkedList with two stars in it consists of a head pointing to the first node and a tail pointing to the last node. As expected, the two nodes hold hands because they are neighbors. I can get to the second element in my list by either going directly to it (through the tail reference) or by going to the first element and asking it for its next neighbor.

If we add a really fat star to the LinkedList above it will look something like this:

I can't get to the second element directly now. I either have to go the the first element and ask to see its next neighbor or go to the last element and ask to see its previous neighbor. Either way, it is bound to be a lot of fun, but I've heard it's hard to balance without a big toe, and these guys don't have any toes. Having them balance stars on their heads seems like a lot to ask, even if they aren't real stars, so in the rest of this discussion we'll draw the LinkedList and Node objects in a way that describes what's going on in memory.
In Textbookish Pictures
Empty LinkedList

LinkedList with One Element

LinkedList with Two Element

LinkedList with Three Elements

In Code
In Java, we declare the LinkedList<E> to implement the List<E> interface and define an inner Node class like this:
public class LinkedList<E> implements List<E> {
private Node head;
private Node tail;
private class Node {
private E value;
private Node next;
private Node prev;
public Node(E val, Node nxt, Node prv) {
value = val;
next = nxt;
prev = prv;
}
}
public LinkedList() {
clear();
}
@Override
public void clear() {
head = null;
tail = null;
}
// ...
Introduction to add() (to the end)
If the list is empty, adding an element involves
- creating a
Nodewith no neighbors and the element referenced as its value. - Pointing the
headof our list to the newly created node - Pointing the
tailof our list to the newly created node
You can see this in the code below and the graphic below the code.
If the list has at least one element, then we leave the head of the list alone, need to tell what used to be the last node in the list that it needs to point to the newly created node (steps 4 and 5 below).
In Code
@Override
public boolean add(E element) {
Node newNode = new Node(element, null, tail); // 1
if(head == null) {
head = newNode; // 2
tail = newNode; // 3
}else{
tail.next = newNode; // 4
tail = newNode; // 5
}
return true;
}
In Pictures
Red indicates what it looked like before the addition and green indicates what it looks like after the addition.


In Song
Fa la la la la
isEmpty() and size()
In Code
@Override
public boolean isEmpty() {
return head==null;
}
@Override
public int size() {
int count = 0;
Node walker = head;
while(walker!=null) {
walker = walker.next;
++count;
}
return count;
}
- We know the list is empty if and only if there is no first element (
head==null), so that's a simple method to implement. - If you were paying attention earlier, you may have noticed that my LinkedList class doesn't keep track of its size. As a result, any time
size()is called, we must walk through all the elements in the list. - This makes size() a O(n) algorithm, but I've chosen to do it this way so we can focus on managing the links in the LinkedList and avoid getting distracted by incrementing and decrementing a size attribute.
Secret Time
- If you've made it this far, you deserve to know at least one of my secrets.
- I don't like doing the same thing over and over again.
- I've noticed that dealing with a linked list means that I'll be walking through it frequently.
- I've made a secret method to help with this1)
- Okay, so how do we keep a secret?2) We'll make it
private.
private Node walkTo(int index) {
if(index<0) {
throw new IndexOutOfBoundsException("Index: " + index);
}
Node walker = head;
int i = 0;
try {
for(; i<index; ++i) {
walker = walker.next;
}
} catch(NullPointerException e) {
throw new IndexOutOfBoundsException("Index: " + index
+ ", Size: " + i);
}
return walker;
}
- I'm catching the
NullPointerExceptionand creating anIndexOutOfBoundsException, why?3) - I can now get a reference to the ith node by calling
walkTo(i-1). - I'll use this secret to implement
get()andset():
@Override
public E get(int index) {
Node node = walkTo(index);
if(node==null) {
throw new IndexOutOfBoundsException("Index: " + index
+ ", Size: " + index);
}
return node.value;
}
@Override
public E set(int index, E element) {
Node node = walkTo(index);
if(node==null) {
throw new IndexOutOfBoundsException("Index: " + index
+ ", Size: " + index);
}
E oldValue = node.value;
node.value = element;
return oldValue;
}
- Notice that if
walkTo(i)returnsnull, it means thatiis one past the end of my list; Therefore, I want to throw anIndexOutOfBoundsexception.
Introduction to add() with an Index
- The
Listinterface contains anaddmethod that specifies where in the list the element should be added. - The first step is for us to walk to the appropriate spot in the list using our handy-dandy, super-secrect
walkTo()method. (step 1 in code) - Once there, we have three possibilities:
- We are at the end of the list
- The
nodereturned isnullif we are at the end of the list because we will have just walked one step past the last node in the list. - Since we already have a method implemented to add to the end of a list, we can just call it.
- The
- We are at the beginning of the list
- We know we are at the beginning if the
nodereturned has aprevwithnullin it. - Here we create a new node, connect it with what used to be the first element in the list, and update the
headof the list (steps 2-4 in code)
- We know we are at the beginning if the
- If we are not at the beginning and not at the end, then we must be somewhere in the middle
- Here we create a new node and introduce it to its new neighbors, who are no longer neighbors (steps 5-7)
- We are at the end of the list
In Code
@Override
public void add(int index, E element) {
Node node = walkTo(index); // 1
if(node==null) { // At end of list
add(element);
} else if(node.prev==null) { // At beginning of list
Node newNode = new Node(element, node, null); // 2
node.prev = newNode; // 3
head = newNode; // 4
} else { // Somewhere in middle of list
Node newNode = new Node(element, node, node.prev); // 5
node.prev = newNode; // 6
newNode.prev.next = newNode; // 7
}
}
In Pictures


Iterators and the LinkedList
- The
Listinterface provides for two types of iterator interfaces:IteratorandListIterator. - A class that implements the
Iteratorinterface must provide three methods:boolean hasNext()— returns true if there is another element accessible to the iterator.E next()— advances the iterator to the next element in the list and returns the value stored there.void remove()— removes the element returned by the last call tonext(). Note: you can only callremove()if you have previously callednext().
- A class that implements the
ListIteratorinterface must do everything anIteratordoes along with a number of other methods. These include:boolean hasPrevious()— returns true if there is a previous element accessible to the iterator.E previous()— returns the value of the element the iterator is currently pointing to and moves the iterator to the previous element in the list.int nextIndex()— returns the location of the next element seen by the iterator as an index value.int previousIndex()— returns the current location of the iterator as an index value.void add(E element)— inserts an element into the list at the current position of the iterator.void set(E element)— replaces the element in the list at the current position of the iterator.
- Since the
ListIteratordoes everything anIteratorcan do, we'll just implement theListIteratorand use it to provide functionality for both theIteratorandListIterator. - We'll need to keep track of three things in our list iterator:
location— Where the iterator is currently pointing (an actual reference to theNodein the list).index— Where the iterator is currently pointing (an integer corresponding to the location in the list).isModifiable— Abooleanindicating whether we can callremove(),add(), orset().
- This class will be an inner class so it will have access to the
LinkedList's attributes.
In Code
private class LLIterator implements ListIterator<E> {
private Node location;
private int index;
private boolean isModifiable;
private LLIterator() {
location = new Node(null, head, null);
index = -1;
isModifiable = false;
}
- We'll continue on with more of this inner class in a moment, but let's take a look at the construtor first.
- When an iterator starts out, it points to one element before the first element in the list (index of -1).
- Since we don't have a
Nodesitting one element before the first element in the list, we need to create one and have it point to the first element in the list, i.e., thehead. - Recall that the
Nodeconstructor takes in(value, next, previous)so thelocationof the iterator is a node with no value whose next node is the first node in the list.
ListIterator.hasNext/Previous()
- If
locationdoesn't have a next, then we are at the end of the list, sohasNext()is just a simple check: - The only time we don't have a previous element is when we are one position before the beginning of the list, so
hasPrevious()is a simple check against our index position.4)
@Override
public boolean hasNext() {
return location.next!=null;
}
@Override
public boolean hasPrevious() {
return index!=-1;
}
ListIterator.next/previousIndex() and ListIterator.set()
nextIndex()andpreviousIndex()are pretty simple too.- Our implementation of
set()doesn't bother to make sure I have a legal index before assigninglocation.valueto the new element. Can you see why it's not needed?
@Override
public int nextIndex() {
return index+1;
}
@Override
public int previousIndex() {
return index;
}
@Override
public void set(E element) {
if(!isModifiable) {
throw new IllegalStateException("Must call next or previous before remove");
}
location.value = element;
}
ListIterator.next/previous()
- Our implementation of
next()callshasNext()to make sure we have something to move to, and then makes the move. previous()is slightly more complicated because we need to treat the situation where we are at the first element in the list with a bit more care.- Suppose I do the following:
- Create a list iterator (at index -1).
- Call next which advances the iterator to index 0 and returns the first element in the list.
- At this point, I should be able to call
previous()— it should return the first element in the list and move to index -1. - That's all good, but to move to index -1, we need to create a temporary
Nodefor the iterator to point to (exactly like we did in the constructor).
@Override
public E next() {
if(!hasNext()) {
throw new NoSuchElementException("No next element available.");
}
isModifiable = true;
location = location.next;
++index;
return location.value;
}
@Override
public E previous() {
if(!hasPrevious()) {
throw new NoSuchElementException("No next element available.");
}
E value = location.value;
if(index==0) {
location = new Node(null, head, null);
index = -1;
isModifiable = false;
} else {
location = location.prev;
--index;
isModifiable = true;
}
return value;
}
ListIterator.remove()
- We now have all but
add()andremove()implemented. - These two are the most complicated of the methods in this class because we need to handle things differently depending on the position of the iterator when add/remove is called.
add()will be left as an exercise for the reader.... enjoy.- In the code below, we have two conditionals.
- The first conditional determines if the iterator is either:
- At the beginning of the list (1) in which case we update the
headof the list to point to the second element in the list, or - Not at the beginning of the list (2) in which case we update the previous node so that it's
nextnow points to one after the iterator.
- At the beginning of the list (1) in which case we update the
- The second conditional determines if the iterator is either:
- At the end of the list (3) in which case we update the
tailof the list to point to the second to last element in the list, or - Not at the end of the list (4) in which case we update the next node so that it's
prevnow points to one before the iterator.
- At the end of the list (3) in which case we update the
In Code
@Override
public void remove() {
if(!isModifiable) {
throw new IllegalStateException("Must call next or previous before remove");
}
if(location.prev==null) { // At beginning of list
LinkedList.this.head = location.next; // 1
} else { // In middle of list
location.prev.next = location.next; // 2
}
if(location.next==null) { // At end of list
LinkedList.this.tail = location.prev; // 3
} else { // In middle of list
location.next.prev = location.prev; // 4
}
isModifiable = false;
}
@Override
public void add(E element) {
throw new UnsupportedOperationException("ListIterator.add() not supported.");
}
}
In Pictures



Iterator-Related Methods in the LinkedList
Meanwhile, back in the outer class, i.e., the LinkedList class, we've got more to do. We can implement the iterator-related methods:
@Override
public Iterator<E> iterator() {
return new LLIterator();
}
@Override
public ListIterator<E> listIterator() {
return new LLIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
if(index<0) {
throw new IndexOutOfBoundsException("Index: " + index);
}
ListIterator<E> itr = new LLIterator();
int i=0;
try {
for(; i<index; ++i) {
itr.next();
}
} catch(NoSuchElementException e) {
throw new IndexOutOfBoundsException("Index: " + index
+ ", Size: " + i);
}
return itr;
}
- Our
LLIteratorclass provides all the functionality for theIteratorinterface, so we can just return anLLIteratorobject for the first method. - The second method is equally simple to implement since the first two return an iterator that points to one before the start of the list.
- The
listIterator(int index)involves one additional step beyond just creating the iterator. Here we need to advance the iterator to positionindex-1.- If
index=0the iterator should point to one before the start of the list (so that when you callnext()you get the element at index 0.) - If
index=1the iterator should point to the start of the list (so that when you callnext()you get the element at index 1.) - If
index=20the iterator should point to the twentieth of the list (so that when you callnext()you get the element at index 20.) - If the index passed in is outside of the range of legitimate values, an
IndexOutOfBoundsExceptionis thrown.
- If
LinkedList.remove()
- Now that we have the iterator defined, we can use it to help implement many of the remaining methods in the
LinkedListclass. - For example, to remove the element at a given index, we can just get an iterator at index (actually one before it), call next, and then use the iterator's remove method:
@Override
public E remove(int index) {
ListIterator<E> itr = listIterator(index);
E value = itr.next();
itr.remove();
return value;
}
- We can also use the iterator to navigate the list while we look for a match to a particular value:
@Override
public boolean remove(Object target) {
boolean removed = false;
ListIterator<E> itr = listIterator();
while(!removed && itr.hasNext()) {
if(target.equals(itr.next())) {
itr.remove();
removed = true;
}
}
return removed;
}
@Override
public boolean contains(Object target) {
Iterator<E> itr = iterator();
boolean found = false;
while(!found && itr.hasNext()) {
found = target.equals(itr.next());
}
return found;
}
@Override
public int lastIndexOf(Object target) {
int index = size();
ListIterator<E> itr = listIterator(index);
boolean found = false;
while(!found && itr.hasPrevious()) {
found = target.equals(itr.previous());
--index;
}
if(!found) {
index = -1;
}
return index;
}
- There are a few other methods that we haven't implemented here, but those will be left as an exercise.
2)... posting it on the internet for the world to see is probably not helping ensure secrecy here.
3)Because
NullPointerException would be caused by walking off the end of the list, which will only happen if we are given an index that is too large for our list.
4)With our implementation, we can't easily use the index to implement
hasNext() since we didn't include a size attribute in the LinkedList class. We could call size(), doing something like: return index<size()-1; but that wouldn't be very efficient. Do you know why? You should.
Last modified: Monday, 29-Jul-2024 06:54:28 EDT