6.3 Linked List Mutation - CSC148 Course Notes
6.3 Linked List Mutation - CSC148 Course Notes
We started with these methods because they’re generally easier to understand than their
mutating cousins. Now, we’re going to look at the two major mutating operations on
linked lists: inserting into and deleting items from a linked list. We’ll start with the simplest
version of this: appending a new item to the end of a linked list. Before we start, let’s
remind ourselves how this works for built-in lists:
>>> lst = []
>>> lst.append(1)
>>> lst.append(2)
>>> lst.append(3)
>>> lst
[1, 2, 3]
Recall that a LinkedList object has only one attribute, a reference to the first node in the
list. Unfortunately, this means that we have some work to do to implement append : before
adding the item, we need to find the currently last node in the linked list, and then add the
item to the end of that. Let’s start (as recommended!!) by using our basic code template:
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""
curr = self._first
while curr is not None:
... curr.item ...
curr = curr.next
This template is a good start, but now our thinking must begin. First: what do we do with
curr.item ? The answer is “Nothing!”—we don’t need to actually use any of the existing
items in the list, and instead are just going through the list to get to the last node.
Unfortunately, there’s a problem with the loop: this loop is designed to keep going until
we’ve processed all of the elements of the list, and curr becomes None . But this is
actually going too far for our purposes: we want to stop the loop as soon as we reach the
last node.[1] We modify our loop condition to check whether the current node is the last
one by using curr.next is None instead.
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""
curr = self._first
while curr.next is not None:
curr = curr.next
At this point, the astute reader will point out a flaw in this change: we aren’t guaranteed
that curr starts off as a node—it could be None . But because we don’t want to get
bogged down with handling that case right now, we’ll add a TODO comment in our code
and keep going.
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""
curr = self._first
while curr.next is not None: # TODO: what if curr starts off as No
curr = curr.next
So then after the loop ends, we know that curr refers to the last node in the linked list,
and we are finally in a position to add the given item to the linked list. To do so, we need to
create a new node and then connect it in.
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""
curr = self._first
while curr.next is not None: # TODO: what is curr starts off as No
curr = curr.next
And finally, let’s handle that TODO. We know from the documentation of our LinkedList
class that self._first can only be None if self refers to an empty linked list. But in
this case, all we need to do is add the new item to be the first item in the linked list.
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""
curr = self._first
if curr is None:
new_node = _Node(item)
self._first = new_node
else:
while curr.next is not None:
curr = curr.next
The first node in the linked list contains the first item
in <items>.
""" Skip to main content
https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 3/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes
self._first = None
for item in items:
self.append(item)
While this code is perfectly correct, it turns out that it is rather inefficient; we’ll leave it as
an exercise for now to develop a better approach.
Index-based insertion
Now suppose we want to implement a more general form of insertion that allows the user
to specify the index of the list to insert a new item into (analogous to the built-in
list.insert method):
class LinkedList:
def insert(self, index: int, item: Any) -> None:
"""Insert a new node containing item at position <index>.
Note: if index == len(self), this method adds the item to the end
of the linked list, which is the same as LinkedList.append.
As with append , our first step is to traverse the list until we reach the correct index; and if
we want the node to be inserted into position index , we need to access the node at
position (index-1) ! To write the code, we need to modify our code template to store not
just the current node, but the current index of that node as well:
def insert(self, index: int, item: Any) -> None:
curr = self._first
curr_index = 0
This loop condition is a bit more complicated, so it’s worth spending some time to unpack.
Here, we’re saying that the loop should keep going when the current node is not None
and when the current index is less than our target index ( index - 1 ). This means that
when the loop is over, the current node is None or the current index has reached the
target index (or both!). We therefore need to structure our code into two cases, and
handle each one separately:
def insert(self, index: int, item: Any) -> None:
curr = self._first
curr_index = 0
Now, if curr is None then the list doesn’t have a node at position index - 1 , and so
that index is out of bounds. In this case, we should raise an IndexError .
On the other hand, if curr is not None , then we’ve reached the desired index, and can
insert the new node using the same strategy as append .
def insert(self, index: int, item: Any) -> None:
curr = self._first
curr_index = 0
Well, almost. The problem with the last else branch is that unlike append , curr might
have had other nodes after it! Simply setting curr.next = new_node loses the reference
to the old node at position index , and any subsequent nodes after that one. So before
Skip to main content
https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 5/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes
overwriting curr.next , we need to update new_node so that it refers to the old node at
position index :
def insert(self, index: int, item: object) -> None:
curr = self._first
curr_index = 0
On the second line, curr.next has already been updated, and its old value lost. The
second line is now equivalent to writing new_node.next = new_node , which is certainly
not what we want!
The reason this type of error is so insidious is that the code looks very similar to the
correct code (only the order of lines is different), and so you can only detect it by carefully
tracing through the updates of the links line-by-line.
To mitigate this problem, we’ll take advantage of a pretty nice Python feature known as
multiple (or simultaneous) assignment:
Skip to main content
https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 6/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes
a, b = 1, 2 # Assigns 1 to a and 2 to b
The beauty of this approach is that the expressions on the right side are all evaluated
before any new values are assigned, meaning that you don’t need to worry about the order
in which you write them. For example, these two assignment statements are equivalent:
# Version 1
curr.next, new_node.next = new_node, curr.next
# Version 2
new_node.next, curr.next = curr.next, new_node
In other words, using multiple assignment in this linked list method allows us to ignore the
tricky business about the order in which the link updates happen! We strongly recommend
using multiple assignment in your own code when working with complex state updating.
The analogue of Python’s list.append is list.pop , which allows the user to remove an
item at a specified index in a list. Because this is quite similar to insertion, we won’t
develop the full code here, but instead outline the basic steps in some pseudo-code:
class LinkedList:
def pop(self, index: int) -> Any:
"""Remove and return node at position <index>.
# 1. If the list is empty, you know for sure that index is out of bo
# 2. Else if index is 0, remove the first node and return its item.
# 3. Else iterate to the (index-1)-th node and update links to remov
# the node at position index. But don't forget to return the item
[1] This is actually a subtle instance of the classic “off-by-one” error in computer
science: our iteration goes for one too few times.
https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 8/8