4.5. Linked Lists¶
4.5.1. Linked Lists¶
In this module we present one of the two traditional implementations for lists, usually called a linked list. The linked list uses dynamic memory allocation, that is, it allocates memory for new list elements as needed. The following diagram illustrates the linked list concept. There are three nodes that are “linked” together. Each node has two boxes. The box on the right holds a link to the next node in the list. Notice that the rightmost node has a diagonal slash through its link box, signifying that there is no link coming out of this box.
Because a list node is a distinct object (as opposed to simply a cell in an array), it is good practice to make a separate list node class. This can either be a standalone class or an inner class, and both have their advantages and disadvantages.
The list built from such nodes is called a singly linked list, or a one-way list, because each list node has a single pointer to the next node on the list.
Our class for linked lists contains two private variables, one pointer to the head of the list, and a variable storing the number of elements. (This second variable is in theory unnecessary, but it improves the efficiency of getting the list size).
class LinkedList<E> implements List<E> {
private Node head; // Pointer to list header
private int listSize; // Size of list
LinkedList() {
head = null;
listSize = 0;
}
class LinkedList(List):
def __init__(self):
self._head = None # Pointer to list header
self._listSize = 0 # Size of list
Here is our implementation for list nodes, an inner private class Node
.
Objects in the Node
class contain a field elem
to
store the element value, and a field next
to store a pointer to
the next node on the list.
private class Node {
E elem; // Value for this node
Node next; // Pointer to next node in list
Node(E elem, Node next) {
this.elem = elem;
this.next = next;
}
}
# Python does not have internal classes, so we have to make the list node standalone.
class LinkedListNode:
def __init__(self, elem, next):
self.elem = elem # Value for this node
self.next = next # Pointer to next node in list
4.5.1.1. Getting and setting values¶
If we want to get or set the value at a certain index, we simply iterate through the nodes in sequence until we get to the node we want.
public E get(int i) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node current = head;
for (int k = 0; k < i; k++)
current = current.next;
return current.elem;
}
public E set(int i, E x) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node current = head;
for (int k = 0; k < i; k++)
current = current.next;
E old = current.elem;
current.elem = x;
return old;
}
def get(self, i):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
current = self._head
for _ in range(i):
current = current.next
return current.elem
def set(self, i, x):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
current = self._head
for _ in range(i):
current = current.next
old = current.elem
current.elem = x
return old
4.5.2. Adding and removing nodes¶
However, if we want to add or remove nodes,
there is a problem with using a pointer to the current
node.
So, using a current
pointer, it is possible to add and remove nodes, using some complicated coding.
But this does not work for the very last node!
There are several possible ways to deal with this problem.
One is to always have an empty node (a “dummy node”) at the very end of the list,
but this will increase memory usage.
Another simple solution is to have a pointer to the node before the current node. This is the solution we will adopt.
4.5.3. Adding a Node¶
Here are some special cases for linked list insertion: Inserting at the beginning of a list, and appending at the end.
Here’s the code for addition.
public void add(int i, E x) {
if (!(0 <= i && i <= listSize)) throw new IndexOutOfBoundsException("list index out of range");
if (i == 0) {
head = new Node(x, head);
} else {
Node prev = head;
for (int k = 0; k < i-1; k++)
prev = prev.next;
prev.next = new Node(x, prev.next);
}
listSize++;
}
def add(self, i, x):
if not (0 <= i <= self._listSize): raise IndexError("list index out of range")
if i == 0:
self._head = LinkedListNode(x, self._head)
else:
prev = self._head
for _ in range(i-1):
prev = prev.next
prev.next = LinkedListNode(x, prev.next)
self._listSize += 1
Here’s an exercise for adding a value to a linked list.
4.5.4. Removing a Node¶
Here’s the code for deletion:
public E remove(int i) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node removed;
if (i == 0) {
removed = head;
head = removed.next;
} else {
Node prev = head;
for (int k = 0; k < i-1; k++)
prev = prev.next;
removed = prev.next;
prev.next = removed.next;
}
removed.next = null; // For garbage collection
listSize--;
return removed.elem;
}
def remove(self, i):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
if i == 0:
removed = self._head
self._head = removed.next
else:
prev = self._head
for _ in range(i-1):
prev = prev.next
removed = prev.next
prev.next = removed.next
removed.next = None # For garbage collection
self._listSize -= 1
return removed.elem
And here’s an exercise.
4.5.5. Complexity analysis¶
Locating a certain position \(i\) in the list requires \(i\) steps. The worst case is if we want to go to the last node, so the time complexity for above all operations is \(\Theta(n)\).
This is much worse than the array-based list, where these operations are \(\Theta(1)\). So are linked lists totally useless? No! But they don’t work well with our current List interface.
To make linked lists useful, we need an enhanced iterator interface, where we can move forwards and backwards in the list, and add/remove nodes through this enhanced iterator. In the standard Java API, this kind of iterator is called a ListIterator, which is part of Java’s standard LinkedList.
4.5.6. Linked List: Full code¶
Finally, here is the full source code for the class LinkedList
.
class LinkedList<E> implements List<E> {
private Node head; // Pointer to list header
private int listSize; // Size of list
LinkedList() {
head = null;
listSize = 0;
}
private class Node {
E elem; // Value for this node
Node next; // Pointer to next node in list
Node(E elem, Node next) {
this.elem = elem;
this.next = next;
}
}
public E get(int i) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node current = head;
for (int k = 0; k < i; k++)
current = current.next;
return current.elem;
}
public E set(int i, E x) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node current = head;
for (int k = 0; k < i; k++)
current = current.next;
E old = current.elem;
current.elem = x;
return old;
}
public void add(int i, E x) {
if (!(0 <= i && i <= listSize)) throw new IndexOutOfBoundsException("list index out of range");
if (i == 0) {
head = new Node(x, head);
} else {
Node prev = head;
for (int k = 0; k < i-1; k++)
prev = prev.next;
prev.next = new Node(x, prev.next);
}
listSize++;
}
public E remove(int i) {
if (!(0 <= i && i < listSize)) throw new IndexOutOfBoundsException("list index out of range");
Node removed;
if (i == 0) {
removed = head;
head = removed.next;
} else {
Node prev = head;
for (int k = 0; k < i-1; k++)
prev = prev.next;
removed = prev.next;
prev.next = removed.next;
}
removed.next = null; // For garbage collection
listSize--;
return removed.elem;
}
public boolean isEmpty() {
return listSize == 0;
}
public int size() {
return listSize;
}
public Iterator<E> iterator() {
return new LinkedListIterator();
}
private class LinkedListIterator implements Iterator<E> {
private Node current;
LinkedListIterator() {
current = head;
}
public boolean hasNext() {
return current != null;
}
public E next() {
E x = current.elem;
current = current.next;
return x;
}
}
}
class LinkedList(List):
def __init__(self):
self._head = None # Pointer to list header
self._listSize = 0 # Size of list
def get(self, i):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
current = self._head
for _ in range(i):
current = current.next
return current.elem
def set(self, i, x):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
current = self._head
for _ in range(i):
current = current.next
old = current.elem
current.elem = x
return old
def add(self, i, x):
if not (0 <= i <= self._listSize): raise IndexError("list index out of range")
if i == 0:
self._head = LinkedListNode(x, self._head)
else:
prev = self._head
for _ in range(i-1):
prev = prev.next
prev.next = LinkedListNode(x, prev.next)
self._listSize += 1
def remove(self, i):
if not (0 <= i < self._listSize): raise IndexError("list index out of range")
if i == 0:
removed = self._head
self._head = removed.next
else:
prev = self._head
for _ in range(i-1):
prev = prev.next
removed = prev.next
prev.next = removed.next
removed.next = None # For garbage collection
self._listSize -= 1
return removed.elem
def isEmpty(self):
return self._listSize == 0
def size(self):
return self._listSize
def __iter__(self):
current = self._head
while current is not None:
yield current.elem
current = current.next
# Python does not have internal classes, so we have to make the list node standalone.
class LinkedListNode:
def __init__(self, elem, next):
self.elem = elem # Value for this node
self.next = next # Pointer to next node in list