CMSC 14100 — Lecture 20

Writing a simple recursive function for a list

Let's try writing a recursive function on a relatively simple problem that we've solved using loops.

Given a list of strings, compute the sum of the lengths of strings in the list.

This is not so bad.


def strings_lengths(lst):
    """
    Given a list of strings, produces the sum of lengths of strings in 
    the list.

    Input:
        lst (list[str]): List of strings

    Output: sum of lengths of strings from lst (int)
    """
    sum = 0
    for s in lst:
        s = s + len(s)
    return sum
    

Now, let's see how we might think about this recursively. Recall that the definition of a list is either empty or not. So we break down our problem into these two cases.

Putting this together, we get the following.


def strings_lengths(lst):
    """
    Given a list of strings, produces the sum of lengths of strings in 
    the list.
    
    Input:
        lst (list[str]): List of strings

    Output: sum of lengths of strings from lst (int)
    """
    if lst == []:
        return 0
    else:
        first, *rest = lst
        return len(first) + strings_lengths(rest)
    

What does this actually look like in action? Well, suppose we try running this on the list ["shoe", "banana", "penguin"].

Another way of seeing this is the following: \begin{align*} &\ \mathtt{strings\_lengths(["shoe", "banana", "penguin"])} \\ =&\ 4 + \mathtt{strings\_lengths(["banana", "penguin"])} \\ =&\ 4 + (6 +\mathtt{strings\_lengths(["penguin"])}) \\ =&\ 4 + (6 + (7 + \mathtt{strings\_lengths([])})) \\ =&\ 4 + (6 + (7 + 0)) \\ =&\ 17 \end{align*}

Sorting

While the previous problem was something we could easily solve with a loop, there are some problems where thinking recursively can be a good idea. Let's consider a classical problem on lists: sorting a list of numbers.

Again, we try to view this problem through the lens of the our recursive definition.

So we have something that looks like


    def our_sort(lst):
        """
        Given a list of integers, produce a new list with the numbers 
        sorted from least to greatest.

        Input:
            lst (list[int]): a list of integers

        Output: list of numbers sorted in increasing order (list[int])
        """
        if lst == []:
            return []
        else:
            first, *rest = lst
            sorted_rest = our_sort(rest)
            ...
            sorted_lst = ???
            return sorted_lst
    

So we're a bit stuck. But what if we think about this as another problem we have to solve recursively? Suppose we have to put an item item into a sorted list. What are our options?

This gives us the following.


def insert(item, lst):
    """
    Inserts a given item into a sorted list.

    Input:
        item (int): the item to be inserted
        lst (list[int]): a sorted list
    
    Output: sorted list containing item and items from lst (list[int])
    """
    if lst == []:
        return [item]
    else:
        first, *rest = lst
        if item < first:
            return [item] + lst
        else:
            return [first] + insert(item, rest)
    

Suppose we tried running insert(5, [1,2,3,8,13,21]). What does this look like?

With less prose, we have \begin{align*} &\ \mathtt{insert(5, [1,2,3,8,13,21])} \\ =&\ \mathtt{[1] + insert(5, [2,3,8,13,21])} \\ =&\ \mathtt{[1] + ([2] + insert(5, [3,8,13,21]))} \\ =&\ \mathtt{[1] + ([2] + ([3] + insert(5, [8,13,21])))} \\ =&\ \mathtt{[1] + ([2] + ([3] + ([5] + [8,13,21])))} \\ =&\ \mathtt{[1,2,3,5,8,13,21])))} \\ \end{align*}

Now, we can complete our strategy for our_sort. If we have a non-empty list, we recursively sort the rest of our list. Then we insert the first element of the list into the rest of the list that's been sorted!


def insertion_sort(lst):
    """
    Given a list of integers, produces a list of the same integers in 
    sorted order.

    Input:
        lst (list[int]): list of integers

    Output: sorted list of integers (list[int])
    """
    if lst == []:
        return []
    else:
        first, *rest = lst
        return insert(first, insertion_sort(rest))
    

Let's see what this looks like with a short list, like [3, 1, 5].

More succinctly, we have \begin{align*} &\ \mathtt{insertion\_sort([3,1,5])} \\ =&\ \mathtt{insert(3, insertion\_sort([1,5]))} \\ =&\ \mathtt{insert(3, insert(1, insertion\_sort([5])))} \\ =&\ \mathtt{insert(3, insert(1, insert(5, insertion\_sort([]))))} \\ =&\ \mathtt{insert(3, insert(1, insert(5, [])))} \\ =&\ \mathtt{insert(3, insert(1, [5]))} \\ =&\ \mathtt{insert(3, [1,5])} \\ =&\ \mathtt{[1,3,5]} \end{align*}

Most treatments of insertion sort involve swapping list items around, but this version shows that it very naturally exploits the recursive structure of lists.