\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

CSC111 Lecture 4: Mutating Linked Lists, Part 2

Linked list quick reference

# Data types (attributes only)
@dataclass
class _Node:
    item: Any
    next: Optional[_Node] = None


class LinkedList:
    _first: Optional[_Node]


# Linked list traveral pattern
curr = self._first
while curr is not None:
    ... curr.item ...

    curr = curr.next

Exercise 1: Value-based deletion

The built-in list data type has another type of deletion: list.remove, which removes the first occurrence of an item. This is known as value-based deletion, since the item that gets removed is based on its value rather than its index.

We will now implement an equivalent LinkedList.remove method:

class LinkedList:
    def remove(self, item: Any) -> None:
        """Remove the first occurence of item from the list.

        Raise ValueError if the item is not found in the list.

        >>> lst = LinkedList([10, 20, 30, 20])
        >>> lst.remove(20)
        >>> lst.to_list()
        [10, 30, 20]
        """
  1. Suppose you want to remove 71 from the linked list below.

    Linked list diagram showing four nodes containing 109, 68, 71, 3.

    1. Modify the diagram above to show how the linked list would change when 71 is removed.

    2. Which node needed to be mutated?

    3. Here is the same linked list. Suppose we now wanted to remove 109. Modify the diagram to show how the linked list needs to change.

      Linked list diagram showing four nodes containing 109, 68, 71, 3.

  2. Using these ideas, implement the LinkedList.remove method.

Exercise 2: Running time of linked list operations

  1. Suppose we have a Python list of length 1,000,000, and a LinkedList of length 1,000,000.

    1. If we want to access the item at index 500,000 in each list, would it be significantly faster for the list, the LinkedList, or about the same amount of time for both? Explain.

    2. If we want to delete the item at index 0 in each list, would it be significantly faster for the list, the LinkedList, or about the same amount of time for both? Explain.

    3. If we want to insert an item at index 500,000 in each list, would it be significantly faster for the list, the LinkedList, or about the same amount of time for both? Explain.

Additional Exercises

  1. Implement a new method LinkedList.remove_all, which is similar to LinkedList.remove but removes all occurrences of the given item from the list (rather than just the first occurrence).

  2. Consider our current implementation of LinkedList.__init__, which uses the append method:

    class LinkedList:
        def __init__(self, items: Iterable) -> None:
            self._first = None
            for item in items:
                self.append(item)
    1. What is the running time of LinkedList.append, in terms of n, the length of the linked list?

    2. Analyse the running time of this implementation of LinkedList.__init__, in terms of \(m\), the length of items. (Note that when the initializer is called, the length of self is 0: the running time only depends on items.)

      Remember that when taking into account the running time of a helper function, you can drop the “Theta”, e.g. count a \(\Theta(n)\) function as \(n\) steps.

    Comment: in this week’s tutorial, you’ll explore how to implement a more efficient LinkedList initializer, but if you’re working on this exercise before your tutorial, please feel free start thinking about it now!