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:
-
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.
-
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.
-
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:
-
How can you define Sum of
n
values recursively (define Sum in terms of Sum)? -
What does the recursive call look like?
-
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).
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:
-
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.
-
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.
-
Do all possible chains of recursion lead to a base case?
-
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