0% found this document useful (0 votes)
11 views

6.3 Linked List Mutation - CSC148 Course Notes

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views

6.3 Linked List Mutation - CSC148 Course Notes

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 8

06/10/2024, 14:24 6.

3 Linked List Mutation — CSC148 Course Notes

6.3 Linked List Mutation


Contents
Linked list append
Index-based insertion
Exercise: Index-based deletion
All of the linked list methods we have looked at so far are non-mutating, meaning they did
not change the linked list upon which they were called. Here’s a reminder of the basic
traversal pattern using a while loop: understanding this is critical before moving on!
# self is a LinkedList
curr = self._first
while curr is not None:
... curr.item ...
curr = curr.next

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]

Linked list append


class LinkedList:
def append(self, item: Any) -> None:
"""Add the given item to the end of this linked list."""

Skip to main content


https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 1/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes

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

# After the loop, curr is the last node in the LinkedList.


# assert curr is not None and curr.next is None

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

# After the loop, curr is the last node in the LinkedList.


# assert curr is not None and curr.next is None

Skip to main content


https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 2/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes

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

# After the loop, curr is the last node in the LinkedList.


# assert curr is not None and curr.next is None
new_node = _Node(item)
curr.next = new_node

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

# After the loop, curr is the last node in the LinkedList.


# assert curr is not None and curr.next is None
new_node = _Node(item)
curr.next = new_node

Example: a more general initializer


With our append method in place, we can now stop creating linked lists by manually
fiddling with attributes, and instead modify our linked list initializer to take in a list of
values, which we’ll then append one at a time:
class LinkedList:
def __init__(self, items: list) -> None:
"""Initialize a new linked list containing the given items.

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>.

Precondition: index >= 0.

Raise IndexError if index > len(self).

Note: if index == len(self), this method adds the item to the end
of the linked list, which is the same as LinkedList.append.

>>> lst = LinkedList([1, 2, 10, 200])


>>> lst.insert(2, 300)
>>> str(lst)
'[1 -> 2 -> 300 -> 10 -> 200]'
>>> lst.insert(5, -1)
>>> str(lst)
'[1 -> 2 -> 300 -> 10 -> 200 -> -1]'
"""

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

while curr is not None and curr_index < index - 1:


curr = curr.next
curr_index += 1

Skip to main content


https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 4/8
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes

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

while curr is not None and curr_index < index - 1:


curr = curr.next
curr_index += 1

# assert curr is None or curr_index == index - 1


if curr is None:
pass
else: # curr_index == index - 1
pass

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

while curr is not None and curr_index < index - 1:


curr = curr.next
curr_index += 1

# assert curr is None or curr_index == index - 1


if curr is None:
# index - 1 is out of bounds. The item cannot be inserted.
raise IndexError
else: # curr_index == index - 1
# index - 1 is in bounds. Insert the new item.
new_node = _Node(item)
curr.next = new_node # Hmm...

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

while curr is not None and curr_index < index - 1:


curr = curr.next
curr_index += 1

# assert curr is None or curr_index == index - 1


if curr is None:
# index - 1 is out of bounds. The item cannot be inserted.
raise IndexError
else: # curr_index == index - 1
# index - 1 is in bounds. Insert the new item.
new_node = _Node(item)
new_node.next = curr.next # THIS LINE IS IMPORTANT!
curr.next = new_node

Warning! Common error ahead! (and solution)


When writing mutating methods on linked lists, we very often update the links of individual
nodes to add and remove nodes in the list. We must be very careful when doing so,
because the order in which we update the links really matters, and often only one order
results in the correct behaviour.
For example, this order of link updates in the final else branch doesn’t work:
curr.next = new_node
new_node.next = curr.next

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.

Tidying up: don’t forget about corner cases!


Our insert implementation has one problem: what if index = 0 ? In this case, it doesn’t
make sense to iterate to the ( index-1 )-th node! This is again a special case which we
need to handle separately, by modifying self._first (because in this case, we’re
inserting into the front of a linked list):
def insert(self, index: int, item: Any) -> None:
if index == 0:
new_node = _Node(item)
self._first, new_node.next = new_node, self._first
else:
curr = self._first
curr_index = 0

while curr is not None and curr_index < index - 1:


curr = curr.next
curr_index += 1

# assert curr is None or curr_index == index - 1


if curr is None:
# index - 1 is out of bounds. The item cannot be inserted.
raise IndexError
else: # curr_index == index - 1
# index - 1 is in bounds. Insert the new item.
new_node = _Node(item)
curr.next, new_node.next = new_node, curr.next

Skip to main content


https://www.teach.cs.toronto.edu/~csc148h/notes/linked-lists/linked_list_mutation.html 7/8
Exercise: Index-based deletion
06/10/2024, 14:24 6.3 Linked List Mutation — CSC148 Course Notes

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>.

Precondition: index >= 0.

Raise IndexError if index >= len(self).

>>> lst = LinkedList([1, 2, 10, 200])


>>> lst.pop(2)
10
>>> lst.pop(0)
1
"""
# Warning: the following is pseudo-code, not valid Python code!

# 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

You might also like