EX10 - Recursive Structures


Overview

In these exercises you will implement a few algorithms to process a singly-linked list data structure. Just like in the List Utility functions, these functions will be pure and are well suited for writing test cases. As such, you will also write test cases demonstrating their usage and verifying their correctness.

Setup

In the comp110/exercises directory, create a directory named ex10_recursion.

linked_list.py

In the ex10_recursion directory, create a file named linked_list.py and establish the following starting contents:

"""Utility functions for working with Linked Lists."""

from __future__ import annotations
from typing import Optional

__author__ = "Your PID"


class Node:
    """An item in a singly-linked list."""
    data: int
    next: Optional[Node]

    def __init__(self, data: int, next: Optional[Node]):
        """Construct a singly linked list. Use None for 2nd argument if tail."""
        self.data = data
        self.next = next


def last(head: Optional[Node]) -> Optional[int]:
    """Returns the last value of a Linked List, or None if the list is empty."""
    return None

linked_list_test.py

Establish a file for your test definitions named linked_list_test.py with the following contents:

"""Tests for linked list utils."""


from comp110.exercises.ex10_recursion.linked_list import Node, last

__author__ = "Your PID"


def test_last_empty():
    """Last of an empty List should be the empty list."""
    assert last(None) == None


def test_last_non_empty():
    """Last of a non-empty list should return a reference to its last Node."""
    linked_list = Node(1, Node(2, Node(3, None)))
    assert last(linked_list) == 3

As you can see, a skeleton definition of last and some simple tests for last are provided to help get you started. You will responsible for defining the other functions and writing their tests.

Requirements

You must use recursive function calls to implement the functions below. If you use loops your work for that function will be disqualified.

last

Given an Option[Node] representing the head Node of a List of any length, including empty, return the value of the last node in the list. If the list is empty, return None. A skeleton definitition is provided with the correct function signature.

value_at

Given a head Option[Node] and an index int as inputs, return the value of the Node stored at the given index, or None if the index does not exist.

Hint #0: In the recursive case, you will need to modify both arguments to bring your recursive call closer to the base case of hint #2. Start by diagramming on paper what this means for a call to value_at with a list of two or more nodes and an initial index of 1.

Hint #1: An edge case occurs when the list is empty. Return None.

Hint #2: A second base case occurs when the index is 0. Here you should return the value of the first node of the list.

Example usage:

>>> value_at(Node(10, Node(20, Node(30, None))), 0)
10
>>> value_at(Node(10, Node(20, Node(30, None))), 1)
20
>>> value_at(Node(10, Node(20, Node(30, None))), 2)
30
>>> value_at(Node(10, Node(20, Node(30, None))), 3)
None

max

Given a head Node, return the maximum value in the linked list. If the linked list is empty, return None.

>>> max(Node(10, Node(20, Node(30, None))))
30
>>> max(Node(10, Node(30, Node(20, None))))
30
>>> max(Node(30, Node(20, Node(10, None))))
30
>>> max(None)
None

Hand-in is Open

You should write your own tests to be confident of your implementations. None of these functions require much code to complete, but will take some mental gymnastics to think about approaching recursively.

Remember: Focus on what you need to do with only the head node and then leave the rest of the problem working through the rest of the list to the recursive function call.

Type Checking Hints

It is possible to write functionally correct programs that do not satisfy the constraints of the type checker. The reason for this a combination of naivete on the type checker’s part, as well as some gnarly theoretical computer science related to the halting problem which you’ll learn about in COMP455.

The short story is a type checker is typically not going to be able to reason through every possible code path in the same way you’re able to. For example, if you have a function that declares it returns Optional[int], and you call that function, then you are going to need to check for the possibility of None as a return value. This is true even if before the function call you made some other test about the argument that convinced you the function call would return an int. Typically this is a sign you can rewrite your logic to be both easier to follow while also satisfying the type checker. Think about making your test of None based on the returned value of the recursive call rather than on the argument before the call is made. Play around with the code and challenge yourself to rewrite it. This is a puzzle and it’s good practice that forces you to grapple with solving the same problem from different angles of attack.