Loops and recursion

Loops and recursion

As a reminder, here is the insertion sort pseudocode:

index = 1
while index < length of list:
  insertion-index = index
  while list[insertion-index] < list[insertion-index - 1]:
    swap list[insertion-index] and list[insertion-index - 1]
    insertion-index = insertion-index - 1
  index = index - 1

And here is our Python implementation:

def insertion_sort(l: list):
  """sorts the input list using the insertion sort algorithm"""
  index = 1
  while index < len(l):
    insertion_index = index
    while insertion_index > 0 and l[insertion_index] < l[insertion_index - 1]:
      # swap the two elements
      element = l[insertion_index]
      l[insertion_index] = l[insertion_index - 1]
      l[insertion_index - 1] = element
      insertion_index = insertion_index - 1
    index = index + 1

The body of the while loop corresponds to the “put the element in the right place” operation we talked about when we were writing pseudocode for insertion sort. What if we wanted to write it as a function, instead? We could call it move_to_correct_location. What arguments should it take? What should its body be?

We might end up with something like this:

def move_to_correct_location(l: list, index: int):
    """Moves the element at index back in lst until it is at the correct location"""
    insertion_index = index
    while insertion_index > 0 and l[insertion_index] < l[insertion_index - 1]:
      # swap the two elements
      element = l[insertion_index]
      l[insertion_index] = l[insertion_index - 1]
      l[insertion_index - 1] = element
      insertion_index = insertion_index - 1

We can get rid of insertion_index, since it’s now redundant with index:

def move_to_correct_location(l: list, index: int):
    """Moves the element at index back in lst until it is at the correct location"""
    while index > 0 and l[index] < l[index - 1]:
      # swap the two elements
      element = l[index]
      l[index] = l[index - 1]
      l[index - 1] = element
      index = index - 1

There’s another way to write this function. In CSCI 0111, you saw two ways of writing functions that operated over lists: for-loops in Python and recursive functions–functions that call themselves–in Pyret. We can rewrite this function to use recursion instead of the while-loop. How might we do that?

Let’s start by getting rid of the while loop. We can do this by turning it into an if. We can also get rid of the statement that increases index by 1:

def move_to_correct_location(l: list, index: int):
    """Moves the element at index back in lst until it is at the correct location"""
    if index > 0 and l[index] < l[index - 1]:
      # swap the two elements
      element = l[index]
      l[index] = l[index - 1]
      l[index - 1] = element

Let’s look at how this (incorrect) function executes on a small example like [8, 9, 7]. When it needs to move the 7 back (i.e., it’s called with 2 as the index), what does it do?

First, we check to see if our index (2) is greater than zero, and that the corresponding element (7) is less than the one at the previous index (8). It is, so we swap the 7 and the 8. Now the list looks like [8, 7, 9]. How can we finish moving the 7 back? Do we have some function that will take the element at index 1 and swap it with the previous element if necessary?

We do–it’s the function we’re writing! We can call move_to_correct_location recursively:

def move_to_correct_location(l: list, index: int):
    """Moves the element at index back in l until it is at the correct location"""
    if index > 0 and l[index] >= l[index - 1]:
      element = l[index]
      l[index] = l[index - 1]
      l[index - 1] = element
      move_to_correct_location(l, index - 1)

We’ve now written a recursive version of our move_to_correct_location function. Something to notice here: we’re not modifying the index variable the way we did in the while case. So how are we moving the element back through the list? The changing argument in calls to move_correct_location plays the role of the index variable!

Recurring on numbers

Let’s talk a bit more about what’s happening here. In Pyret, we saw lots of recursive functions on data structures like lists (and trees!). When we wrote recursive functions on lists, we always had two cases: one for the empty list, and one for a link (some element linked to the rest of the list). Our recursive function here looks a little different: we’re passing in the same list each time. The second argument to the function, though, is a number (an array index, specifically), and it changes with every call. One way to think about it is that just as a list can be either:

  • the empty list or
  • a link,

an array index can be either:

  • 0
  • 1 + n, where n is another index

How does this work in our move_to_correct_location function? In the zero case, we immediately return–we don’t need to do any work. In the 1+n case, we move the element back from 1+n if necessary, then call ourselves on the index n.

More fun with while loops and recursion

Factorial

The factorial function comes up frequently in probability and statistics (among other disciplines). The factorial of n (written n! is the product of the first n integers; for instance,

3! = 3 * 2 * 1 = 6

How would we implement factorial using loops? How about recursion?

def factorial_loop(n: int) -> int:
    total = 1
    while n > 1:
        total = total * n
        n = n - 1
    return total

def factorial_rec(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial_rec(n - 1)