CS21 Week 12: Recursion

Week 12 Topics

  • Recursive functions

Monday

Finishing Sorting

Before moving on to new content, let’s wrap up a few things about sorting from last week.

Recursion

Any function that sometimes calls itself is known as a recursive function. In most cases, recursion can be considered an alternative to iteration (loops). Instead of defining a loop structure, recursion defines a problem as a smaller problem until it reaches an end point. There are three requirements for a successful recursive function:

  1. Base case: each function must have one or more base cases where the function does not make a recursive call to itself. Instead, the problem has reached its smallest point, and we can begin returning back the small solutions.

  2. Recursive case: the function makes one (or more) calls to itself. The call must use a different argument to the function (or else we keep making the same recursive call over and over again!). Typically, the problem gets 1 smaller.

  3. All possible recursive calls must be guaranteed to hit a base case. If you miss one, your program is susceptible to an infinite recursion bug!

Example: Print a Message

The printmessage.py program prints a given message n times. We’ve already seen how to solve this problem with a loop (this is known as an iterative solution, one that uses a loop to repeat). However, let’s explore how we can solve it without a loop using recursion.

To think about how to define printing a message n times in a recursive way, we want to define printing in terms of printing. In other words we want to think of a recursive definition of printing n things, and our recursive function’s structure will follow this with it recursive function calls:

printmessage(msg, num_times) is equivalent to:

  1. print(msg)
  2. printmessage(msg, num_times-1)  # here is our recursive definition

To understand what’s happening with recursion, it’s often helpful to draw a stack diagram. The key concept is that Python treats the recursive function call the same as any other function call — it places a new stack frame on top and begins executing the new function. The only difference is the function in this case has the same name.

Example: Summing Integers

In recursivefuncs.py we are going to write some iterative and recursive versions of the same function. Iterative versions use loops, recursive versions do not use loops, and instead contain calls to themselves but on a smaller problem.

I’ve given you the code for an algorithm we have seen before — a for loop that sums all integers between 1 and n:

def iterative_sum(n):

    sum = 0
    for i in range(1, n+1):
        sum = sum + i
    return sum

Together, we’ll write a recursive function that accomplishes the same task. Here are some questions to consider:

  1. How can you define Sum of n values recursively (define Sum in terms of Sum)?

  2. What does the recursive call look like?

  3. What is the base case?

Test out your solution on different values calling both the iterative and your recursive version in main. You can print out their return values to check that they return the same values for the same passed argument value. (and remember that CNTRL-C will kill a program stuck in an infinite loop (or stuck in infinite recursion)).

After getting your recursiveSum to work, next try implementing an iterative (with a loop) and a recursive version of a function that computes \(n!\) (n factorial).

\[n! = n \cdot (n-1) \cdot (n-2) \cdot \ldots \cdot 1 \\ 3! = 3 \cdot 2 \cdot 1 = 6 \\ 5! = 5 \cdot 4 \cdot 3 \cdot 2 \cdot 1 = 120 \\\]

Add some calls in main to your functions to test them out.

Command line arguments

Often time programers write programs that have command line arguments, values that are included in the command line to run the program. For example,

$ python3 cmdlineargs.py 13 4.5 Freya

In this example, the program 13, 4.5, and "Freya" are values that are passed into the program cmdlineargs.py.

In the program can use the values of these command line arguments by getting their values through the system variable sys.argv which is a list of string values, one per command line argument.

sys.argv[0] is "cmdlineargs.py"  # the name of the program is always element 0
sys.argv[1] is "13"
sys.argv[2] is "4.5"
sys.argv[3] is "Freya"

Let’s look at cmdlineargs.py and see what it is doing. Then try running it with different command line arguments.

Wednesday

Tips for Recursion

For any recursive problem, think of these questions:

  1. What is the base case? In what instances is the problem small enough that we can solve the problem directly? Sometimes you may need more than one base case, but rarely do you need several.

  2. What is the recursive call? Make one or calls in the function to itself. It is very important to use different arguments (usually "smaller") in the recursive call otherwise you obtain infinite recursion.

  3. Do all possible chains of recursion lead to a base case?

  4. If a recursive function is supposed to return a value, make sure all cases return a value and that recursive calls do something with the return value of the smaller problem before returning.

Recursion on Strings and Lists

We are going to look at recursive functions on strings and lists.

Slicing (creating substrings and sublists)

Before moving on to more recursion examples, we’ll briefly remind you about a Python operator called slicing that can be particularly useful when working with recursion. Slicing lets you easily look at subsets of strings or lists. The general syntax is mystr[i:j]. This creates a new string that is the substring of mystr starting at index i and running up to but not including index j.

$ python3
>>> hw = "hello world"
>>> hw[1]
'e'
>>> hw[7:10]
'orl'
>>> hw[0:5]
'hello'
>>> hw[6:]
'world'
>>> hw[:5]
'hello'

You can also omit i or j. mystr[i:] creates the substring starting at index i and running to the end of that string. mystr[:j] gives you the substring up to but not including the j-th character.

Note: slicing can also be used with lists e.g. if lst = [1,2,3,4,5], then lst[2:4] creates a new list [3,4]:

lst = [1,2,3,4,5]
sublst = lst[2:4]   # sublist refers to new list [3,4]

Let’s look at string_rec.py at the print_string function to see what it is doing.

Exercise: String Length

Try implementing string_len_rec function that recursively computes the length of the passed string values (this could be how Python implements the len function).

Exercise: Number of Even Values

Next, we’ll use recursion to work with lists. In numevens_rec.py, write an iterative and a recursive version of a function that takes a list of int values and returns the number of values in the list that are even.

Remember that the mod operator (%) can be used to test for evenness"

  10 % 2   # is 0 the remainder of dividing 10 by 2
  11 % 2   # is 1 the remainder of dividing 11 by 2
  12 % 2   # is 0 the remainder of dividing 12 by 2
  ...

Friday

Recursions on Lists (and strings) the better way)

Recursion that we have done so far on lists and strings, creates new sublists or substrings on every recursive call. For large-sized lists, this uses a lot of space (e.g., starting with a list with 1,000 element, first recursive call creates a new sublist with with 999 elements, the next create a new sublist with 998 elements, …​, last call creates an empty list).

In addition, because list elements can be modified in-place, passing a sublist to a recursive function mean that the function cannot directly modify the elements in the list that is passed to it as an argument.

To solve both of these problems, we can modify the recursive function to recurse in-place on the list (and we can recurse in-place on strings too in a similar way).

A recursive function that recurses in-place on a passed list (or string), takes one additional parameter: the next index value into the list that specifies the list element on which to perform the function’s action.

The first call to this type of recursive function typically passes in 0 or n-1 (where n is the length of the list), and then either increments or decrements the index value passed to recursive calls (depending on which order the list elements are processed (low to high or high to low)). The recursion typically stops when the index is out of bounds.

For example, to compute the sum of the element in a list using in-place recursion, the function definition looks like:

def rec_sum(lst, index):

In the file, list_rec.py let’s look at this example and try it out. Draw the stack to see what it is doing.

Exercise

Add a recursive that squares all the values in a passed list.

def rec_square(lst, index):
  """
  squares all the values in the list of values
    lst: list of numeric values
    index: the index of the element to square next
    returns: None (this function has side-effects)
  """

Add some calls to your recursive function from main to test it out.

A Code Design Aside

This is not important for code you write in this class, however, often times when designing code that may be part of a library of functions, a programmer wants to hide the details of an in-place recursive implementation of the function from the user (i.e., the programmer using our library should not need to pass in a starting index value…​that just seems weird).

To implement a cleaner interface to users of our function, while still implementing it using in-place recursion on a list, we implement a wrapper-function around our recursive function. The wrapper-function is the nice interface to the user, that then just makes a call to the recursive function that does the actual work. For example:

def square_list(lst):
   """
   A library function that squares all the values in the passed list
     lst: a list of int values
     returns: None
   """

   return rec_square(lst, 0)   # hides the recursive function implementation