Recursive Algorithms¶
Recursive Functions¶
Defining mathematical functions recursively is similar to to rule based programming. A program is a sequence of rules. Each rule has certains conditions that must be met in order for the rule to apply.
Consider the recursive definition of the factorial function. Let \(n\) be a natural number. By \(n!\) we denote the factorial of \(n\). Its recursive definition is given by two rules:
- For \(n \leq 1\): \(n! = 1\).
- If we know the value for \((n-1)!\), then \(n! = n \times (n-1)!\).
Recursion is similar to mathematical proof by induction:
- First we verify the trivial or base case.
- Assuming the statement holds for all values smaller than \(n\) – the induction hypothesis – we extend the proof to \(n\).
In Python, we can define the factorial recursively as in the function below.
def factorial(nbr):
"""
Computes the factorial of nbr recursively.
"""
if nbr <= 1:
return 1
else:
return nbr*factorial(nbr-1)
def main():
"""
Prompts the user for a number
and returns the factorial of it.
"""
nbr = int(input('give a number n : '))
fac = factorial(nbr)
print('n! = ', fac)
print('len(n!) = ', len(str(fac)))
To understand how recursive functions work,
let us trace the execution of the recursive function factorial
,
with 5
as argument.
factorial(5) call #0: call for n-1 = 4
factorial(4) call #1: call for n-1 = 3
factorial(3) call #2: call for n-1 = 2
factorial(2) call #3: call for n-1 = 1
factorial(1) call #4: base case, return 1
factorial(2) call #3: returning 2
factorial(3) call #2: returning 6
factorial(4) call #1: returning 24
factorial(5) call #0: returning 120
Observe that the computations happen in the return statements:
return 1
, return 1*2
, return 1*2*3
, return 1*2*3*4
,
and return 1*2*3*4*5
.
When running the recursive factorial, we may run into the limit of the stack size.
$ python factorial.py
give a number : 79
n! = 894618213078297528685144171539831652
069808216779571907213868063227837990693501
860533361810841010176000000000000000000
len(n!) = 117
Exploiting Python long integers, a bit more:
$ python factorial.py
give a number : 1234
...
RuntimeError: maximum recursion depth exceeded
What just happened? Why did we encounter this exception?
The execution of recursive functions
requires a stack of function calls.
For example, for n = 5
, the stack grows like
factorial(1) call #4: base case, return 1
factorial(2) call #3: returning 2
factorial(3) call #2: returning 6
factorial(4) call #1: returning 24
factorial(5) call #0: returning 120
New function calls are pushed on the stack. Upon return, a function call is popped off the stack.
As an exception handler, we will compute the factorial iteratively.
def factexcept(nbr):
"""
When the recursion depth is exceeded
the factorial of nbr is computed iteratively.
"""
if nbr <= 1:
return 1
else:
try:
return nbr*factexcept(nbr-1)
except RuntimeError:
print('run time error raised')
fac = 1
for i in range(2, nbr+1):
fac = fac*i
return fac
Accumulating Parameters¶
An accumulating parameter accumulates the result of a computation. With accumulating parameters, we can trace recursive functions automatically.
Tracing the execution of a recursive function means: displaying for each function call:
- the value(s) for the input parameter(s);
- what is computed inside the function; and
- the return value of the function.
As an example of an accumulating parameter,
let us keep track of the number of each function call.
Use an accumulating parameter k
:
def factotrace(nbr, k):
The we increment k
with each recursive call:
return factotrace(nbr-1, k+1)
To illustrate the running of the function factotrace
,
we call factotrace
with nbr = 5
and k = 0
, which then prints
factotrace(5,0): call for nbr-1 = 4
factotrace(4,1): call for nbr-1 = 3
factotrace(3,2): call for nbr-1 = 2
factotrace(2,3): call for nbr-1 = 1
factotrace(1,4): base case, return 1
factotrace(2,3): returning 2
factotrace(3,2): returning 6
factotrace(4,1): returning 24
factotrace(5,0): returning 120
At call k
, we indent with k
spaces.
The code for the function comes next.
def factotrace(nbr, k):
"""
Prints out trace information in call k
the initial value for k should be zero.
"""
prt = k*' '
prt = prt + 'factotrace(%d,%d):' % (nbr, k)
if nbr <= 1:
print(prt + ' base case, return 1')
return 1
else:
print(prt + ' call for nbr-1 = ' + str(nbr-1))
fac = nbr*factotrace(nbr-1, k+1)
print(prt + ' returning %d' % fac)
return fac
Instead of performing the multiplications in the return of our first recursive function for the factorial, we can execute the multiplications with an accumulating parameter. Consider the following function.
def factaccu(nbr, fac):
"""
Accumulates the factorial of nbr in fac
call factaccu initially with fac = 1.
"""
if nbr <= 1:
return fac
else:
return factaccu(nbr-1, fac*nbr)
Let us trace the execution of factaccu
,
for nbr = 5
and fac = 1
on input.
factaccu(5,1) call #0: call for nbr-1 = 4
factaccu(4,5) call #1: call for nbr-1 = 3
factaccu(3,20) call #2: call for nbr-1 = 2
factaccu(2,60) call #3: call for nbr-1 = 1
factaccu(1,120) call #4: returning 120
factaccu(2,60) call #3: returning 120
factaccu(3,20) call #2: returning 120
factaccu(4,5) call #1: returning 120
factaccu(5,1) call #0: returning 120
We observe that factaccu
1*5
, 1*5*4
, 1*5*4*3
, 1*5*4*3*2
, and
then returns 120.
If we extend factaccu
with another accumulating parameter
to count the number of function calls, we can have the function
print all tracing statements. Consider the function below.
def factatrace(nbr, fac, k):
"""
Accumulates the factorial of nbr in fac,
k is used to trace the calls
initialize fac to 1 and k to 0.
"""
prt = k*' '
prt = prt + 'factatrace(%d,%d,%d)' % (nbr, fac, k)
if nbr <= 1:
print(prt + ' returning ' + str(fac))
return fac
else:
print(prt + ' call for nbr-1 = ' + str(nbr-1))
result = factatrace(nbr-1, fac*nbr, k+1)
print(prt + ' returning %d' % result)
return result
The output of factatrace
, called with
nbr = 5
, fac = 1
, and k = 0
is below
factatrace(5,1,0) call for nbr-1 = 4
factatrace(4,5,1) call for nbr-1 = 3
factatrace(3,20,2) call for nbr-1 = 2
factatrace(2,60,3) call for nbr-1 = 1
factatrace(1,120,4) returning 120
factatrace(2,60,3) returning 120
factatrace(3,20,2) returning 120
factatrace(4,5,1) returning 120
factatrace(5,1,0) returning 120
Recursive Problem Solving¶
If reading a word forwards and backwards is the same,
then the word is a palindrome.
Examples of palindromes are mom, dad, rotor.
The problem is to write a function which,
given a word, returns True
if the word is palindrom,
and return False
otherwise.
Using the builtin methods of Python, we can solve this problem quickly. Consider the session below.
>>> s = 'motor'
>>> L = [c for c in s]
>>> L
['m', 'o', 't', 'o', 'r']
>>> L.reverse()
>>> L
['r', 'o', 't', 'o', 'm']
>>> t = ''.join(L)
>>> t
'rotom'
>>> s == t
False
Next we derive a recursive solution.
def is_palindrome(word):
"""
Returns True if word is a palindrome,
and returns False otherwise.
"""
We have three base cases:
- The word is empty or only one character long.
- The first and last character are different.
- The word consists of two equal characters.
These three base cases are implemented in the first three if statements of the function below.
def is_palindrome(word):
"""
Returns True if word is a palindrome,
and returns False otherwise.
"""
if len(word) <= 1:
return True
elif word[0] != word[len(word)-1]:
return False
elif len(word) == 2:
return True
else:
short = word[1:len(word)-1]
return is_palindrome(short)
The main function used to test is_palindrome
follows.
def main():
"""
Prompts the user for a word and checks
if it is a palindrome.
"""
word = input('give a word : ')
prt = 'the word \"' + word + '\" is '
if not is_palindrome(word):
prt = prt + 'not '
prt = prt + 'a palindrome'
print(prt)
if __name__ == "__main__":
main()
We end with some examples of running the script palindromes.py
at the command line.
$ python3 palindromes.py
give a word : palindromes
the word "palindromes" is not a palindrome
Because of the input()
returns a string,
we can also test numbers:
$ python3 palindromes.py
give a word : 1234321
the word "1234321" is a palindrome
The palindrome tester works just as well on numbers.
Exercises¶
- The n-th Fibonacci number \(F_n\) is defined
for \(n \geq 2\) as \(F_n = F_{n-1} + F_{n-2}\)
and \(F_0 = 0\), \(F_1 = 1\).
Give a Python function
Fibonacci
to compute \(F_n\). - Use an accumulating parameter
k
toFibonacci
to count the function calls. When tracing the execution, print withk
spaces as indentations. - Write an equivalent C function to compute factorials recursively. Use a main interactive program to test it.
- Extend
is_palindrome
with an extra accumulating parameterk
to keep track of the function calls. Trace the execution with this extended function, usingk
spaces as indentations. - Write a recursive function to sum a list of numbers.