Linked Lists
The first structure you build from nothing. No Python list underneath — just objects holding references to other objects. It trades away instant indexing and buys constant-time insertion anywhere you already are.
- Build a linked list from node objects.
- Insert and delete without losing the rest of the list.
- Say precisely what linked lists are better and worse at.
- Handle the empty list and the head as separate cases.
A Python list stores items contiguously, so inserting in the middle shifts everything after it. A linked list stores each item wherever, connected by references, so inserting is three assignments regardless of size.
File systems chain disk blocks this way. Undo histories, music playlists and the free-memory lists inside an allocator are linked structures, chosen because insertion and removal dominate.
Linked lists were invented in 1955 for an early artificial-intelligence program at RAND. The idea — data scattered in memory but logically ordered by references — is the ancestor of every tree and graph in this course.
Nodes
class _Node:
"""A node in a linked list.
Attributes:
item: the value stored here
next: the following node, or None at the end of the list
"""
item: object
next: object
def __init__(self, item) -> None:
"""Initialize a node holding item with no successor."""
self.item = item
self.next = Nonenext being None is what marks the
end. Every traversal stops on it.class LinkedList:
"""A sequence of items stored in linked nodes.
Attributes:
_first: the first node, or None if the list is empty
"""
_first: object
def __init__(self) -> None:
"""Initialize an empty linked list."""
self._first = None
def is_empty(self) -> bool:
"""Return whether this list is empty."""
return self._first is None
def __len__(self) -> int:
"""Return the number of items in this list.
>>> lst = LinkedList()
>>> len(lst)
0
"""
count = 0
node = self._first
while node is not None:
count = count + 1
node = node.next
return count__len__ is $O(n)$: there is no stored
count, so it walks the whole list. A Python list answers in $O(1)$. That is
the first of the trade-offs.Almost every linked-list method is this loop:
node = self._first
while node is not None:
# do something with node.item
node = node.nextForgetting the last line gives an infinite loop, and it is the single most common mistake in the unit.
Insertion
def prepend(self, item) -> None:
"""Add item to the front of this list.
>>> lst = LinkedList()
>>> lst.prepend(2)
>>> lst.prepend(1)
>>> len(lst)
2
"""
new_node = _Node(item)
new_node.next = self._first
self._first = new_nodeinsert(0, x) is $O(n)$.self._first first
would lose the reference to the old head and orphan the entire
list.# wrong - the rest of the list is now unreachable self._first = new_node new_node.next = self._first # points at itself!
Always attach the new node to the existing structure before redirecting anything that pointed into it. Drawing the arrows makes this obvious; reading the code does not.
def insert_after(self, target, item) -> bool:
"""Insert item immediately after the first occurrence of target.
Return whether target was found.
>>> lst = LinkedList()
>>> lst.prepend(1)
>>> lst.insert_after(1, 2)
True
"""
node = self._first
while node is not None:
if node.item == target:
new_node = _Node(item)
new_node.next = node.next
node.next = new_node
return True
node = node.next
return FalseDeletion
def remove(self, item) -> bool:
"""Remove the first occurrence of item and return whether it was found.
>>> lst = LinkedList()
>>> lst.prepend(1)
>>> lst.remove(1)
True
>>> lst.is_empty()
True
"""
if self._first is None:
return False
if self._first.item == item: # removing the head
self._first = self._first.next
return True
node = self._first # removing anything else
while node.next is not None:
if node.next.item == item:
node.next = node.next.next
return True
node = node.next
return Falseself._first plays that
role.node.next, not
node, because you must hold the node before the one you
are deleting.Nothing is erased. Setting node.next = node.next.next routes
around the unwanted node, and Python's garbage collector reclaims it once
nothing refers to it.
The trade-off
Neither is better. Choose by which operation dominates, which is exactly the analysis Unit 9 formalises.
Unit 3's queue had an $O(n)$ dequeue. Keeping a reference to the last node as well as the first fixes it.
_last, then
update _last. $O(1)$._first.
$O(1)$.• Forgetting node = node.next and looping
forever.
• Reassigning references in the wrong order and orphaning the
list.
• Not handling the empty list or the head as separate
cases.
• Comparing with == None rather than
is None.
• Expecting len() to be free. It walks the whole
list.
Practice
Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.
How does a traversal know it has finished?
Answer
next is None.What breaks if you set self._first = new_node before
new_node.next = self._first?
Answer
What is the cost of getting item $k$?
Answer
Why does remove treat the head separately?
Answer
_first reference
has to be updated instead.Write a method adding an item at the end.
Solution
def append(self, item) -> None:
"""Add item to the end of this list."""
new_node = _Node(item)
if self._first is None:
self._first = new_node
return
node = self._first
while node.next is not None:
node = node.next
node.next = new_nodeCount how many times an item appears.
Solution
def count(self, item) -> int:
"""Return how many times item appears in this list."""
total = 0
node = self._first
while node is not None:
if node.item == item:
total = total + 1
node = node.next
return totalReverse a linked list in place.
Solution
def reverse(self) -> None:
"""Reverse the order of the items in this list."""
previous = None
node = self._first
while node is not None:
following = node.next # save before overwriting
node.next = previous
previous = node
node = following
self._first = previousfollowing first is essential — overwriting
node.next destroys the only route to the
rest.Give a case where a linked list clearly beats a Python list.
Answer
Expanded topic checklist
Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:
- Nodes. Item plus reference, with
Nonemarking the end. - Traversal. The standard loop and why the advance line matters.
- Insertion and deletion. Assignment order, and the head and empty cases.
- Trade-offs. What linked lists win and lose against a Python list.
The calendar names "linked data structures" explicitly. Check Quercus for the assigned exercises; linked-list manipulation is a standard term test question and is usually assessed by writing code by hand.
Objects holding references to objects. Change the structure by redirecting references, in the right order. Units 7 and 8 use the same idea with two references per node instead of one.
Deep practice extensions
Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.
What signals the end of a linked list?
Guide
next of
None. Every traversal loop tests for
it.Why is inserting at a known node $O(1)$?
Guide
Why is removing the first item special?
Guide
next can be redirected, so the list's own
_first must change.When does a Python list win?